001: /*
002: * DO NOT ALTER OR REMOVE COPYRIGHT NOTICES OR THIS HEADER.
003: *
004: * Copyright 1997-2007 Sun Microsystems, Inc. All rights reserved.
005: *
006: * The contents of this file are subject to the terms of either the GNU
007: * General Public License Version 2 only ("GPL") or the Common
008: * Development and Distribution License("CDDL") (collectively, the
009: * "License"). You may not use this file except in compliance with the
010: * License. You can obtain a copy of the License at
011: * http://www.netbeans.org/cddl-gplv2.html
012: * or nbbuild/licenses/CDDL-GPL-2-CP. See the License for the
013: * specific language governing permissions and limitations under the
014: * License. When distributing the software, include this License Header
015: * Notice in each file and include the License file at
016: * nbbuild/licenses/CDDL-GPL-2-CP. Sun designates this
017: * particular file as subject to the "Classpath" exception as provided
018: * by Sun in the GPL Version 2 section of the License file that
019: * accompanied this code. If applicable, add the following below the
020: * License Header, with the fields enclosed by brackets [] replaced by
021: * your own identifying information:
022: * "Portions Copyrighted [year] [name of copyright owner]"
023: *
024: * Contributor(s):
025: *
026: * The Original Software is NetBeans. The Initial Developer of the Original
027: * Software is Sun Microsystems, Inc. Portions Copyright 2004-2005 Sun
028: * Microsystems, Inc. All Rights Reserved.
029: *
030: * If you wish your version of this file to be governed by only the CDDL
031: * or only the GPL Version 2, indicate your decision by adding
032: * "[Contributor] elects to include this software in this distribution
033: * under the [CDDL or GPL Version 2] license." If you do not indicate a
034: * single choice of license, a recipient has the option to distribute
035: * your version of this file under either the CDDL, the GPL Version 2 or
036: * to extend the choice of license to its licensees as provided above.
037: * However, if you add GPL Version 2 code and therefore, elected the GPL
038: * Version 2 license, then the option applies only if the new code is
039: * made subject to such option by the copyright holder.
040: */
041:
042: package org.netbeans.modules.junit;
043:
044: import javax.swing.JTextField;
045: import javax.swing.event.ChangeEvent;
046: import javax.swing.event.ChangeListener;
047: import javax.swing.event.DocumentEvent;
048: import javax.swing.event.DocumentListener;
049: import javax.swing.text.BadLocationException;
050:
051: /**
052: * Text-field that validates whether its text is a valid class name (may be
053: * package-classified or not) and may notify a registered listener of status
054: * changes (empty/valid/invalid).
055: * <p>
056: * To start listening on validity of the entered class name, register
057: * a <code>ChangeListener</code>.
058: * <p>
059: * Example:
060: * <pre><code> ...
061: * final ClassNameTextField tfClassName = new ClassNameTextField();
062: * tfClassName.setChangeListener(new ChangeListener() {
063: * public void stateChanged(ChangeEvent e) {
064: * int state = tfClassName.getState();
065: * switch (state) {
066: * case ClassNameTextField.STATUS_EMPTY:
067: * System.out.println("Empty class name!");
068: * break;
069: * case ClassNameTextField.STATUS_INVALID:
070: * System.out.println("Invalid class name!");
071: * break;
072: * case ClassNameTextField.VALID:
073: * System.out.println("Thank you!");
074: * break;
075: * }
076: * }
077: * });
078: * panel.add(tfClassName);
079: * ...
080: * </code></pre>
081: *
082: * @author Marian Petras
083: */
084: public final class ClassNameTextField extends JTextField {
085:
086: /** status: the class name is valid */
087: public static final int STATUS_VALID = 0;
088: /** status: the class name is empty */
089: public static final int STATUS_EMPTY = 1;
090: /** status: the class name is not valid */
091: public static final int STATUS_INVALID = 2;
092: /** */
093: public static final int STATUS_VALID_NOT_DEFAULT = 3;
094:
095: /**
096: * internal status - when the text is empty or when it ends with a dot
097: * (<code>'.'</code>) and appending one legal character to it would make
098: * it a legal class name
099: */
100: static final int STATUS_BEFORE_PART = 3;
101:
102: /** */
103: private TextListener documentListener;
104: /** */
105: private int externalStatus = 0;
106: /** */
107: private boolean externalStatusValid = false;
108: /** */
109: private ChangeListener changeListener;
110: /** */
111: private ChangeEvent changeEvent;
112: /** */
113: private String defaultText;
114:
115: /**
116: * Creates an empty text-field.
117: */
118: public ClassNameTextField() {
119: this ((String) null);
120: setupDocumentListener();
121: }
122:
123: /**
124: * Creates an empty with initial text.
125: *
126: * @param text initial text of the text-field
127: * (for empty text, use <code>""</code>
128: * or <code>null</code>)
129: */
130: public ClassNameTextField(String text) {
131: super (text == null ? "" : text); //NOI18N
132: setupDocumentListener();
133: }
134:
135: /**
136: */
137: public void setDefaultText(String defaultText) {
138: if ((defaultText == null) && (this .defaultText == null)
139: || (defaultText != null)
140: && defaultText.equals(this .defaultText)) {
141: return;
142: }
143:
144: this .defaultText = defaultText;
145:
146: if ((defaultText != null)
147: || (externalStatusValid && (externalStatus == STATUS_VALID_NOT_DEFAULT))) {
148: statusMaybeChanged();
149: }
150: }
151:
152: /**
153: */
154: private void setupDocumentListener() {
155: getDocument().addDocumentListener(
156: documentListener = new TextListener());
157: }
158:
159: /**
160: * Determines internal status for the current text.
161: * The status may be one of
162: * <code>STATUS_VALID</code>, <code>STATUS_INVALID</code> and
163: * <code>STATUS_BEFORE_PART</code>.
164: *
165: * @return status for the current text
166: */
167: int determineStatus() {
168: String text = getText();
169:
170: int status = STATUS_BEFORE_PART;
171: char[] chars = text.toCharArray();
172: for (int i = 0; i < chars.length; i++) {
173: char c = chars[i];
174: switch (status) {
175: case STATUS_BEFORE_PART:
176: if (!Character.isJavaIdentifierStart(c)) {
177: return STATUS_INVALID;
178: }
179: status = STATUS_VALID;
180: break;
181: case STATUS_VALID:
182: if (c == '.') {
183: status = STATUS_BEFORE_PART;
184: } else if (Character.isJavaIdentifierPart(c)) {
185: status = STATUS_VALID;
186: } else {
187: return STATUS_INVALID;
188: }
189: break;
190: default:
191: assert false;
192: }
193: }
194: return status;
195: }
196:
197: /**
198: * Returns status of the text.
199: *
200: * @return one of <code>STATUS_EMPTY</code>,
201: * <code>STATUS_VALID</code>,
202: * <code>STATUS_INVALID</code>
203: */
204: public int getStatus() {
205: if (!externalStatusValid) {
206: updateExternalStatus();
207: }
208: return externalStatus;
209: }
210:
211: /**
212: */
213: private void updateExternalStatus() {
214: assert externalStatusValid == false;
215:
216: int internalStatus = documentListener.status;
217: switch (internalStatus) {
218: case STATUS_VALID:
219: externalStatus = (defaultText == null)
220: || defaultText.equals(getText()) ? STATUS_VALID
221: : STATUS_VALID_NOT_DEFAULT;
222: break;
223: case STATUS_BEFORE_PART:
224: externalStatus = (getText().length() == 0) ? STATUS_EMPTY
225: : STATUS_INVALID;
226: break;
227: case STATUS_INVALID:
228: externalStatus = STATUS_INVALID;
229: break;
230: default:
231: assert false;
232: externalStatus = STATUS_INVALID;
233: break;
234: }
235: externalStatusValid = true;
236: }
237:
238: /**
239: * Registers a change listener.
240: * The listener will be notified each time status of this text-field
241: * (valid/invalid/empty) changes.
242: *
243: * <!-- PENDING: The listener cannot be unregistered. -->
244: * <!-- PENDING: Only one listener can be registered at a time. -->
245: *
246: * @param listener change listener to be registered
247: * @see #getStatus
248: */
249: public void setChangeListener(ChangeListener listener) {
250: changeEvent = new ChangeEvent(this );
251: this .changeListener = listener;
252: }
253:
254: /**
255: */
256: private void statusMaybeChanged() {
257: externalStatusValid = false;
258:
259: if (changeListener != null) {
260: final int prevExternalStatus = externalStatus;
261: externalStatus = getStatus();
262: if (externalStatus != prevExternalStatus) {
263: changeListener.stateChanged(changeEvent);
264: }
265: }
266: }
267:
268: /**
269: */
270: private final class TextListener implements DocumentListener {
271:
272: /** internal status of the class name */
273: private int status;
274: /** */
275: private int length;
276:
277: /**
278: */
279: public TextListener() {
280: status = determineStatus();
281: length = ClassNameTextField.this .getText().length();
282: }
283:
284: /**
285: */
286: public void changedUpdate(DocumentEvent documentEvent) {
287: length = documentEvent.getDocument().getLength();
288: int newStatus = determineStatus();
289:
290: if (newStatus != status) {
291: status = newStatus;
292: statusMaybeChanged();
293: } else if ((status == STATUS_VALID)
294: && (defaultText != null)) {
295: statusMaybeChanged(); //maybe default <--> not default
296: }
297:
298: assert length == getDocument().getLength();
299: }
300:
301: /**
302: */
303: public void insertUpdate(DocumentEvent documentEvent) {
304: int newStatus;
305: boolean wasEmpty = (length == 0);
306:
307: if (documentEvent.getLength() != 1
308: || (documentEvent.getOffset() != length)) {
309: length += documentEvent.getLength();
310: newStatus = determineStatus();
311: } else {
312: char c;
313:
314: /* now we know that a single character was appended */
315: try {
316: c = documentEvent.getDocument()
317: .getText(length++, 1).charAt(0);
318: switch (status) {
319: case STATUS_VALID:
320: newStatus = (c == '.') ? newStatus = STATUS_BEFORE_PART
321: : (Character.isJavaIdentifierPart(c)) ? STATUS_VALID
322: : STATUS_INVALID;
323: break;
324: case STATUS_BEFORE_PART:
325: newStatus = (Character.isJavaIdentifierStart(c)) ? STATUS_VALID
326: : STATUS_INVALID;
327: break;
328: case STATUS_INVALID:
329: newStatus = determineStatus();
330: break;
331: default:
332: assert false;
333: newStatus = determineStatus();
334: break;
335: }
336: } catch (BadLocationException ex) {
337: assert false;
338:
339: length = documentEvent.getDocument().getLength();
340: newStatus = determineStatus();
341: }
342: }
343:
344: /*
345: * We must handle addition of a text to an empty text field
346: * specially because it may not change internal state
347: * (if it becomes STATUS_BEFORE_PART after the addition).
348: */
349: if ((newStatus != status) || wasEmpty) {
350: status = newStatus;
351: statusMaybeChanged();
352: } else if ((status == STATUS_VALID)
353: && (defaultText != null)) {
354: statusMaybeChanged(); //maybe default <--> not default
355: }
356:
357: assert length == getDocument().getLength();
358: }
359:
360: /**
361: */
362: public void removeUpdate(DocumentEvent documentEvent) {
363: int newStatus;
364:
365: if (documentEvent.getLength() != 1
366: || (documentEvent.getOffset() != (length - 1))) {
367: length -= documentEvent.getLength();
368: newStatus = determineStatus();
369: } else {
370:
371: /*
372: * now we know that a single character was deleted
373: * from the end
374: */
375: length--;
376: switch (status) {
377: case STATUS_VALID:
378: try {
379: newStatus = ((length == 0) || (documentEvent
380: .getDocument().getText(length - 1, 1))
381: .charAt(0) == '.') ? STATUS_BEFORE_PART
382: : STATUS_VALID;
383: } catch (BadLocationException ex) {
384: assert false;
385:
386: newStatus = determineStatus();
387: length = documentEvent.getDocument()
388: .getLength();
389: }
390: break;
391: case STATUS_BEFORE_PART:
392: newStatus = STATUS_VALID; //trailing dot deleted
393: break;
394: case STATUS_INVALID:
395: newStatus = (length == 0) ? STATUS_VALID
396: : determineStatus();
397: break;
398: default:
399: assert false;
400: newStatus = determineStatus();
401: break;
402: }
403: }
404:
405: /*
406: * We must handle deletion of the whole text specially because
407: * it may not change internal state (if it was STATUS_BEFORE_PART
408: * before the deletion).
409: */
410: if ((newStatus != status) || (length == 0)) {
411: status = newStatus;
412: statusMaybeChanged();
413: } else if ((status == STATUS_VALID)
414: && (defaultText != null)) {
415: statusMaybeChanged(); //maybe default <--> not default
416: }
417:
418: assert length == getDocument().getLength();
419: }
420:
421: }
422:
423: }
|