001: /*************************************************************************
002: * *
003: * 1) This source code file, in unmodified form, and compiled classes *
004: * derived from it can be used and distributed without restriction, *
005: * including for commercial use. (Attribution is not required *
006: * but is appreciated.) *
007: * *
008: * 2) Modified versions of this file can be made and distributed *
009: * provided: the modified versions are put into a Java package *
010: * different from the original package, edu.hws; modified *
011: * versions are distributed under the same terms as the original; *
012: * and the modifications are documented in comments. (Modification *
013: * here does not include simply making subclasses that belong to *
014: * a package other than edu.hws, which can be done without any *
015: * restriction.) *
016: * *
017: * David J. Eck *
018: * Department of Mathematics and Computer Science *
019: * Hobart and William Smith Colleges *
020: * Geneva, New York 14456, USA *
021: * Email: eck@hws.edu WWW: http://math.hws.edu/eck/ *
022: * *
023: *************************************************************************/package edu.hws.jcm.awt;
024:
025: import edu.hws.jcm.data.*;
026: import java.awt.*;
027: import java.awt.event.*;
028:
029: /**
030: * A VariableInput is an input box into which the user can type a real
031: * number value, which becomes the value of an associated Variable.
032: * The value of the Variable can change only when the VariableInput's
033: * checkInput() method is called (usually by a Controller). See the Controller
034: * class for more information.
035: *
036: * <p>Whenever checkInput is called, an error of type JCMError
037: * might be generated. If throwErrors is true, this error is
038: * thrown; if it is false, the error is caught, the value
039: * of the variable is set to Double.NaN, and no error is thrown.
040: * The error message associated with the error can be retrieved by
041: * calling getErrorMessage(), if desired. (This value is null if
042: * no error occurred the last time checkInput was called.)
043: *
044: * <p>A VariableInput is a Value, so it can be used directly where
045: * a Value object is needed.
046: *
047: * <p>An VariableInput will ordinarily be registered with
048: * a Controller in TWO ways: It's added to a Controller
049: * with the Controller's add() method. This makes the Controller
050: * call the VariableInput's checkInput() method during the
051: * Controller's compute() method. Secondly, a Controller
052: * is set as the "onUserAction" property. This causes the
053: * Controller's compute() method to be called when the user
054: * presses return in the VariableInput box. This is optional--
055: * you might, for example, only want the Controller to compute()
056: * when a Compute button is pressed. You can also set the
057: * VariableInput's onTextChange property to a Controller
058: * that you want to compute every time the text in the box
059: * changes.
060: *
061: * <p>After the VariableInput is created, it is possible to specify the
062: * largest and smallest allowed values for the variable. It is also
063: * possible to specify what sytle of input is allowed. The style
064: * can be to allow any constant expression, constant real numbers only,
065: * or integers only. Set these parameters with setMin(), setMax(),
066: * and setInputStyle(). For setInputStyle(), the legal parameter
067: * values are VariableInput.EXPRESSION, VariableInput.REAL, and
068: * VariableInput.INTEGER. The default input style is EXPRESSION.
069: *
070: */
071: public class VariableInput extends TextField implements InputObject,
072: Tieable, Value {
073:
074: /**
075: * The Variable that represents
076: * the value of this input box. (VI is
077: * a private nested class inside VariableInput.)
078: */
079: protected VI variable;
080:
081: /**
082: * True if an error should be thrown
083: * when checkInput() is calles and
084: * the contents do not define a
085: * legal number. True by default.
086: */
087: protected boolean throwErrors;
088:
089: /**
090: * Error message from the most recent
091: * time checkInput() as called.
092: * Null if there was no error.
093: */
094: protected String errorMessage;
095:
096: /**
097: * This serial number is increased
098: * each time the value of the variable
099: * changes.
100: */
101: protected long serialNumber;
102:
103: /**
104: * This is set to true if the text in the
105: * box has been changed since the last time
106: * the value of the variable was checked by checkInput().
107: */
108: //protected boolean hasChanged;
109: protected String previousContents;
110:
111: private Controller onUserAction; // If this is non-null, the compute() method
112: // of onUserAction is called when the user
113: // presses return in this input-box.
114:
115: private Controller onTextChange; // If this is non-null, the compute() method
116: // of onTextChange is called when the text
117: // in this input box changes
118:
119: /**
120: * Smallest allowable value.
121: */
122: protected double minValue = -Double.MAX_VALUE;
123:
124: /**
125: * Largest allowable value.
126: */
127: protected double maxValue = Double.MAX_VALUE;
128:
129: /**
130: * One of the constant values EXPRESSION, REAL, or
131: * INTEGER, specifying the style of input.
132: */
133: protected int inputStyle = 0;
134:
135: /**
136: * A constant for use in the setInputStyle() method. Any constant expression is allowed.
137: */
138: public static final int EXPRESSION = 0;
139:
140: /**
141: * A constant for use in the setInputStyle() method. Only real numbers are allowed.
142: */
143: public static final int REAL = 1;
144:
145: /**
146: * A constant for use in the setInputStyle() method. Only integers are allowed.
147: */
148: public static final int INTEGER = 2;
149:
150: /**
151: * Create an unnamed VariableInput with initial contents "0".
152: */
153: public VariableInput() {
154: this (null, null);
155: }
156:
157: /**
158: * Construct a VariableInput with the given name
159: * and initial String (which can both be null).
160: * If initialString is null, the string "0" is used.
161: * No error occurs in the constructor if the initialString
162: * does not represent a legal value (A string rather than a
163: * double is used for initialization since the initial
164: * content can be an expression such as "pi/2".)
165: * If name is not null, it is used as the name of
166: * the VariableInput component as well as the name
167: * of the associated variable.
168: */
169: public VariableInput(String name, String initialString) {
170: super ((initialString == null) ? "0" : initialString, 12);
171: setBackground(Color.white);
172: variable = new VI(name);
173: if (name != null)
174: super .setName(name);
175: //hasChanged = true;
176: previousContents = null;
177: variable.checkInput(); // Won't throw an error, since throwErrors is false.
178: //enableEvents(AWTEvent.KEY_EVENT_MASK);
179: throwErrors = true;
180: }
181:
182: /**
183: * Create a VariableInput just as in the constructor
184: * VariableInput(String,String). Then, if both parser and
185: * name are non-null, register the associated variable
186: * with the parser.
187: */
188: public VariableInput(String name, String initialString,
189: Parser parser) {
190: this (name, initialString);
191: addTo(parser);
192: }
193:
194: /**
195: * Get the associated variable for the VariableInput box. You will need
196: * this, for example, if you want to register the variable with a Parser.
197: */
198: public Variable getVariable() {
199: return variable;
200: }
201:
202: /**
203: * Convenience method for creating a component containing
204: * this VariableInput together with a label of the form
205: * "<name> = ". This version uses default colors for the
206: * label, which are inherited from the containing
207: * component.
208: */
209: public JCMPanel withLabel() {
210: return withLabel(null, null);
211: }
212:
213: /**
214: * Convenience method for creating a component containing
215: * this VariableInput together with a label of the form
216: * "name = ". Uses the given background and foreground
217: * colors for the label and the panel. The colors can be
218: * null to use the defaults, which will be inherited from the
219: * containing Component.
220: */
221: public JCMPanel withLabel(Color back, Color fore) {
222: Label label = new Label(" " + variable.getName() + " =");
223: JCMPanel panel = new JCMPanel();
224: if (back != null) {
225: panel.setBackground(back);
226: label.setBackground(back);
227: }
228: if (fore != null) {
229: panel.setForeground(fore);
230: label.setBackground(fore);
231: }
232: panel.add(label, BorderLayout.WEST);
233: panel.add(this , BorderLayout.CENTER);
234: return panel;
235: }
236:
237: /**
238: * Set the name of the variable. This should not be called
239: * while the variable is registered with a Parser.
240: * The name of the VariableInput Component is also set to name,
241: * if the name is non=null.
242: */
243: public void setName(String name) {
244: variable.setName(name);
245: if (name != null)
246: super .setName(name);
247: }
248:
249: /**
250: * A convenience method that registers this VariableInput's variable
251: * with Parser p (but only if both p and the name of the variable are non-null).
252: */
253: public void addTo(Parser p) {
254: if (p != null && variable.getName() != null)
255: p.add(variable);
256: }
257:
258: /**
259: * If the Controller, c, is non-null, then its compute() method will be called whenever
260: * the user presses the return key while typing in this text-input box.
261: */
262: public void setOnUserAction(Controller c) {
263: onUserAction = c;
264: enableEvents(AWTEvent.ACTION_EVENT_MASK);
265: }
266:
267: /**
268: * Return the Controller, if any, that is notified when the user
269: * presses return in this text-input box.
270: */
271: public Controller getOnUserAction() {
272: return onUserAction;
273: }
274:
275: /**
276: * Method required by InputObject interface; in this class, it simply calls
277: * setOnUserAction(c). This is meant to be called by JCMPanel.gatherInputs().
278: */
279: public void notifyControllerOnChange(Controller c) {
280: setOnUserAction(c);
281: }
282:
283: /**
284: * If the Controller, cm is non-null, then its compute() method will be called whenever
285: * the text in this input box changes. Furthermore, the throwErrors
286: * property will be set to false, to avoid throwing multiple errors
287: * while the user is typing. (You can change it back to true if
288: * you want by calling setThrowErrors(true).)
289: */
290: public void setOnTextChange(Controller c) {
291: onTextChange = c;
292: enableEvents(AWTEvent.TEXT_EVENT_MASK);
293: if (c != null)
294: throwErrors = false;
295: }
296:
297: /**
298: * Return the Controller, if any, that is notified when the text
299: * in this input box changes
300: */
301: public Controller getOnTextChange() {
302: return onTextChange;
303: }
304:
305: /**
306: * Return the value of the associated variable, which might not
307: * reflect the value of the contents of the input box. The value
308: * of the variable changes only when the checkInput() method is called,
309: * or when the setVal() method is called.
310: * Call checkInput() first, if you want to be sure of getting the
311: * same value that is currently shown in the box.
312: */
313: public double getVal() {
314: return variable.getVal();
315: }
316:
317: /**
318: * Set the value of the associated variable.
319: * Also sets the content of the input box.
320: */
321: public void setVal(double d) {
322: variable.setVal(d);
323: }
324:
325: /**
326: * Set the throwErrors property. If the value is true, then
327: * an error will be thrown by the checkInput() method when the
328: * contents of the VariableInput box are not legal. Otherwise,
329: * no error is thrown; the value of the variable is just set
330: * to Double.NaN.
331: */
332: public void setThrowErrors(boolean throwErrors) {
333: this .throwErrors = throwErrors;
334: }
335:
336: /**
337: * Return the value of the throwErrors property.
338: */
339: public boolean getThrowErrors() {
340: return throwErrors;
341: }
342:
343: /**
344: * Specify the smallest allowed value for the content of this VariableInput box.
345: */
346: public void setMin(double min) {
347: if (!Double.isNaN(min)) {
348: minValue = min;
349: //hasChanged = true;
350: previousContents = null;// (force recheck of contents)
351: }
352: }
353:
354: /**
355: * Return the minimum value that will be accepted in this VariableInput box.
356: */
357: public double getMin() {
358: return minValue;
359: }
360:
361: /**
362: * Specify the largest allowed value for the content of this VariableInput box.
363: */
364: public void setMax(double max) {
365: if (!Double.isNaN(max)) {
366: maxValue = max;
367: //hasChanged = true;
368: previousContents = null;// (force recheck of contents)
369: }
370: }
371:
372: /**
373: * Return the maximum value that will be accepted in this VariableInput box.
374: */
375: public double getMax() {
376: return maxValue;
377: }
378:
379: /**
380: * Specify what types of things are allowed in the input box.
381: * The value of the parameter, style, must be one of the constants VariableInput.EXPRESSION,
382: * VariableInput.REAL, or VariableInput.INTEGER. If not, the call to setInputStyle is ignored.
383: */
384: public void setInputStyle(int style) {
385: if (style == EXPRESSION || style == REAL || style == INTEGER) {
386: if (style != inputStyle) {
387: //hasChanged = true;
388: previousContents = null;// (force recheck of contents)
389: inputStyle = style;
390: }
391: }
392: }
393:
394: /**
395: * Return the input style, which determines what types of things
396: * are allowed in the input box. The returned value is one
397: * of the contstants EXPRESSION, REAL, or INTEGER
398: */
399: public int getInputStyle() {
400: return inputStyle;
401: }
402:
403: /**
404: * Get error message from previous call to checkInput().
405: * Returns null if there was no error.
406: */
407: public String getErrorMessage() {
408: return errorMessage;
409: }
410:
411: //--------------------- Implementation Details -------------------------------------------
412:
413: /**
414: * Check whether the contents are valid, and change the value
415: * of the associated variable if the new contents do not match
416: * the current value. This might throw an error of type JCMError,
417: * if throwErrors is true. This is usually called by a Controller.
418: */
419: public void checkInput() {
420: variable.checkInput();
421: }
422:
423: /**
424: * Return this object's serial number, which increases whenever the
425: * value of the associated variable changes.
426: */
427: public long getSerialNumber() {
428: return serialNumber;
429: }
430:
431: /**
432: * Synchronize serial number and value with newest, unless
433: * this VariableInput is itself newest. This is required by
434: * the Tieable interface, and is usually called by an object of type Tie.
435: */
436: public void sync(Tie tie, Tieable newest) {
437: if (newest == this )
438: return;
439: if (!(newest instanceof Value))
440: throw new IllegalArgumentException(
441: "Internal Error: A VariableInput can only sync with Value objects.");
442: variable.setVal(((Value) newest).getVal());
443: serialNumber = newest.getSerialNumber();
444: }
445:
446: private class VI extends Variable {
447: // This class is used to define a Variable object associated
448: // with this VariableInput.
449: VI(String name) {
450: super (name);
451: }
452:
453: public void setVal(double d) {
454: // If d is different from the current value of the variable,
455: // set the value of the variable to d, set the displayed text,
456: // and increment the serial number of the variable.
457: double oldVal = this .getVal();
458: boolean hasChanged = previousContents == null
459: || !previousContents.equals(getText());
460: if (!hasChanged
461: && ((Double.isNaN(d) && Double.isNaN(oldVal)) || (d == oldVal)))
462: return; // Value is not actually changing.
463: serialNumber++;
464: justSetText(NumUtils.realToString(d));
465: //hasChanged = false;
466: previousContents = getText();
467: errorMessage = null;
468: super .setVal(d);
469: }
470:
471: void checkInput() {
472: // If the contents of the input box have changed, change
473: // the value of the variable to match. If this is an actual
474: // change in the value of the variable, then the serialNumber
475: // is incremented.
476: boolean hasChanged = previousContents == null
477: || !previousContents.equals(getText());
478: if (!hasChanged)
479: return;
480: errorMessage = null;
481: String content = getText();
482: try {
483: double d = convertInput(content);
484: double oldVal = this .getVal();
485: if ((Double.isNaN(d) && Double.isNaN(oldVal))
486: || (d == oldVal))
487: return; // Value is not actually changing.
488: serialNumber++;
489: super .setVal(d);
490: } catch (JCMError e) {
491: if (!Double.isNaN(this .getVal()))
492: serialNumber++;
493: super .setVal(Double.NaN); // Value becomes undefined.
494: if (throwErrors)
495: throw e;
496: }
497: }
498: }
499:
500: private transient Parser constantParser; // To be used to process constant expressions;
501:
502: // will not know about any variables or user functions.
503:
504: /**
505: * Convert a string into a real value. The parameter is taken from the input box when
506: * this method is called by VI.checkInput()
507: * Throw a JCMError if any error is found in the input.
508: *
509: * @param num String to be converted
510: * @return the real value.
511: */
512: protected double convertInput(String num) {
513: double ans = Double.NaN; // The value.
514: if (inputStyle == EXPRESSION) {
515: if (constantParser == null)
516: constantParser = new Parser();
517: try {
518: Expression exp = constantParser.parse(num);
519: ans = exp.getVal();
520: } catch (ParseError e) {
521: errorMessage = "Illegal constant expression: "
522: + e.getMessage();
523: if (throwErrors) {
524: setCaretPosition(e.context.pos);
525: requestFocus();
526: }
527: }
528: } else if (inputStyle == REAL) {
529: try {
530: Double d = new Double(num);
531: ans = d.doubleValue();
532: } catch (NumberFormatException e) {
533: errorMessage = "Value is not a legal real number.";
534: if (throwErrors) {
535: requestFocus();
536: }
537: }
538: } else { // inputStyle is INTEGER
539: try {
540: ans = Long.parseLong(num);
541: } catch (NumberFormatException e) {
542: errorMessage = "Value is not a legal integer.";
543: if (throwErrors) {
544: requestFocus();
545: }
546: }
547: }
548: if (errorMessage == null) {
549: if (ans < minValue || ans > maxValue) {
550: errorMessage = "Value outside legal range. It should be ";
551: if (inputStyle == INTEGER)
552: errorMessage += "an integer ";
553: else if (inputStyle == REAL)
554: errorMessage += "a real number ";
555: if (minValue > -Double.MAX_VALUE
556: && maxValue < Double.MAX_VALUE)
557: errorMessage += "between "
558: + NumUtils.realToString(minValue) + " and "
559: + NumUtils.realToString(maxValue);
560: else if (minValue > -Double.MAX_VALUE)
561: errorMessage += "greater than or equal to "
562: + NumUtils.realToString(minValue);
563: else
564: errorMessage += "less than or equal to "
565: + NumUtils.realToString(maxValue);
566: if (throwErrors) {
567: requestFocus();
568: }
569: }
570: }
571: if (errorMessage != null)
572: throw new JCMError(errorMessage, this );
573: return ans;
574: }
575:
576: /*
577: * Override processKeyEvent to only allow characters
578: * that are legal in this VariableInput.
579: *
580: * @param evt used internally.
581: public void processKeyEvent(KeyEvent evt) {
582: if (evt.getID() == KeyEvent.KEY_PRESSED) {
583: int ch = evt.getKeyCode();
584: char chr = evt.getKeyChar();
585: boolean use = (chr != 0 && Character.isDigit(chr)
586: || chr == '-' || chr == '+')
587: || ch == KeyEvent.VK_DELETE
588: || ch == KeyEvent.VK_BACK_SPACE;
589: if (inputStyle != INTEGER)
590: use = use || chr == '.' || chr == 'e' || chr == 'E';
591: if (inputStyle == EXPRESSION)
592: use = use || Character.isLetter(chr)
593: || chr == '(' || chr == ')' || chr == '*'
594: || chr == '/' || chr == '^'
595: || chr == ':' || chr == '?' || chr == '|'
596: || chr == '&' || chr == '~' || chr == '='
597: || chr == '<' || chr == '>' || chr == '!'
598: || ch == KeyEvent.VK_SPACE;
599: boolean useControl = use || ch == KeyEvent.VK_TAB
600: || ch ==KeyEvent.VK_ENTER || chr == 0;
601: if (!useControl) {
602: evt.consume();
603: Toolkit.getDefaultToolkit().beep();
604: }
605: else if (use) {
606: hasChanged = true;
607: }
608: }
609: super.processKeyEvent(evt);
610: }
611: */
612:
613: /**
614: * This overrides the setText() method from the TextField class so that
615: * it will also force the contents to be checked the next time
616: * the checkInput() method is called.
617: *
618: * @param text change text to this.
619: */
620: public void setText(String text) {
621: super .setText(text);
622: //hasChanged = true;
623: previousContents = null;
624: }
625:
626: private void justSetText(String text) {
627: // Call super.setText().
628: super .setText(text);
629: }
630:
631: /**
632: * Overridden to call onUserAction.compute() if onUserAction is non-null.
633: * This is not meant to be called directly.
634: */
635: public void processActionEvent(ActionEvent evt) {
636: if (onUserAction != null)
637: onUserAction.compute();
638: super .processActionEvent(evt);
639: }
640:
641: /**
642: * Overridden to call onUserAction.compute() if onUserAction is non-null.
643: * This is not meant to be called directly.
644: */
645: public void processTextEvent(TextEvent evt) {
646: //hasChanged = true;
647: previousContents = null;
648: if (onTextChange != null)
649: onTextChange.compute();
650: super .processTextEvent(evt);
651: }
652:
653: } // end class VariableInput
|