001: /*
002: * Jatha - a Common LISP-compatible LISP library in Java.
003: * Copyright (C) 1997-2005 Micheal Scott Hewett
004: *
005: * This library is free software; you can redistribute it and/or
006: * modify it under the terms of the GNU Lesser General Public
007: * License as published by the Free Software Foundation; either
008: * version 2.1 of the License, or (at your option) any later version.
009: *
010: * This library is distributed in the hope that it will be useful,
011: * but WITHOUT ANY WARRANTY; without even the implied warranty of
012: * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU
013: * Lesser General Public License for more details.
014: *
015: * You should have received a copy of the GNU Lesser General Public
016: * License along with this library; if not, write to the Free Software
017: * Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA 02111-1307 USA
018: *
019: *
020: * For further information, please contact Micheal Hewett at
021: * hewett@cs.stanford.edu
022: *
023: */
024: package org.jatha.display;
025:
026: import org.jatha.dynatype.LispValue;
027: import org.jatha.Jatha;
028:
029: import javax.swing.*;
030: import javax.swing.event.CaretListener;
031: import javax.swing.event.CaretEvent;
032: import javax.swing.text.*;
033: import java.awt.*;
034: import java.awt.event.ActionListener;
035: import java.awt.event.KeyListener;
036: import java.awt.event.ActionEvent;
037: import java.awt.event.KeyEvent;
038: import java.util.Iterator;
039: import java.util.ArrayList;
040:
041: // @date Wed Mar 5 09:03:03 1997
042: /**
043: * LispInput is a text field that does parenthesis
044: * matching and sends its input off to a Lisp
045: * Evaluator.
046: *
047: * @see java.awt.TextField
048: * @author Micheal S. Hewett hewett@cs.stanford.edu
049: *
050: */
051: class LispInput extends JPanel implements Runnable, ActionListener,
052: KeyListener {
053: public static boolean DEBUG = false;
054:
055: /* ------------------ PRIVATE variables ------------------------------ */
056: protected JTextArea f_inputArea = null;
057:
058: protected int f_matchingPosition = -1;
059: protected boolean f_flashing = false;
060: protected Thread f_myThread = null;
061: protected Graphics f_myGraphics = null;
062: protected String f_input;
063:
064: // GUI stuff
065: JButton f_largerAreaButton = new JButton("larger");
066: JButton f_smallerAreaButton = new JButton("smaller");
067: JButton f_evalButton = new JButton("EVAL");
068:
069: // Font info
070: protected FontMetrics fontInfo = null;
071: protected int fontWidth = 0;
072: protected int fontHeight = 0;
073: protected Color fgColor = null;
074: protected Color bgColor = null;
075:
076: protected int hFudge = 8;
077: protected int vFudge = 7;
078:
079: protected Listener f_parent;
080:
081: protected String f_saveBuffer = "";
082: protected String f_lastCommand = "";
083: protected boolean f_firstCharOfCommand = true;
084: protected int f_commandMultiplier = 1;
085:
086: protected Font f_defaultFont = new Font("Courier", Font.PLAIN, 12);
087:
088: // Used when matching parentheses
089: protected Highlighter.HighlightPainter f_goodPainter = new DefaultHighlighter.DefaultHighlightPainter(
090: Color.cyan);
091: protected Highlighter.HighlightPainter f_badPainter = new DefaultHighlighter.DefaultHighlightPainter(
092: Color.magenta);
093:
094: protected java.util.List f_highlights = new ArrayList(10);
095: protected Integer f_highlightLock = new Integer(17);
096:
097: /**
098: * Edit this to change the size of the input area.
099: * (mh) 2005 Nov 06
100: */
101: protected int f_defaultNumberOfInputLines = 15;
102:
103: private Jatha f_lisp = null;
104:
105: /* ------------------ CONSTRUCTOR ------------------------------ */
106:
107: public LispInput(Jatha lisp, Listener parent, int rows, int cols) {
108: super ();
109:
110: f_lisp = lisp;
111: f_parent = parent;
112:
113: this .setLayout(new BoxLayout(this , BoxLayout.Y_AXIS));
114: setBackground(new Color(170, 170, 170)); // new Color(255, 255, 153)); // yellow
115:
116: // Set up the input area
117: f_inputArea = new JTextArea(rows, cols);
118: f_inputArea.setFont(f_defaultFont);
119: f_inputArea.addKeyListener(this );
120: f_inputArea.addCaretListener(new BracketMatcher(this ));
121:
122: JScrollPane scroller = new JScrollPane(f_inputArea,
123: JScrollPane.VERTICAL_SCROLLBAR_ALWAYS,
124: JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED);
125: add(scroller);
126:
127: f_largerAreaButton.addActionListener(this );
128: f_largerAreaButton.setActionCommand("largerArea");
129: f_largerAreaButton.setBackground(this .getBackground());
130:
131: f_smallerAreaButton.addActionListener(this );
132: f_smallerAreaButton.setActionCommand("smallerArea");
133: f_smallerAreaButton.setBackground(this .getBackground());
134:
135: f_evalButton.addActionListener(this );
136: f_evalButton.setActionCommand("eval");
137: f_evalButton.setBackground(this .getBackground());
138:
139: // Submit/Expand/Shrink buttons
140: Box buttonPanel = new Box(BoxLayout.X_AXIS);
141: buttonPanel.add(f_evalButton);
142: buttonPanel.add(Box.createHorizontalGlue());
143: buttonPanel.add(f_largerAreaButton);
144: buttonPanel.add(f_smallerAreaButton);
145:
146: add(buttonPanel);
147:
148: /*
149: myThread = new Thread(this, "Parenthesis Matching");
150: myThread.start();
151: */
152: }
153:
154: /* ------------------ PUBLIC methods ------------------------------ */
155:
156: // Set the font - can't do this until it is visible.
157: public void setFontInfo() {
158: f_myGraphics = this .getGraphics();
159:
160: fontInfo = this .getFontMetrics(this .getFont());
161: fontWidth = fontInfo.charWidth('A');
162: fontHeight = fontInfo.getHeight();
163: fgColor = this .getForeground();
164: bgColor = this .getBackground().brighter(); // Motif is doing something to us.
165:
166: // hFudge = fontWidth - 2;
167: }
168:
169: /**
170: * Input should be a regular string, which is parsed and evaluated.
171: */
172: public void eval(String inputString) {
173: f_parent.message(inputString + "\n", true);
174: LispValue result = f_lisp.load(inputString, true);
175: if (result.basic_null())
176: f_parent.message(
177: ";; *** ERROR - unbalanced parentheses in input\n",
178: false);
179: else {
180: f_parent.addHistoryItem(inputString);
181:
182: // f_parent.message(f_lisp.eval(input)); // result will be printed in the Output window.
183: f_lastCommand = inputString;
184:
185: // Select all the text in the input box
186: setText(f_lastCommand);
187: clearHighlights();
188: selectAll();
189: }
190:
191: // LispParser parser = new LispParser(f_lisp, inputString + " ");
192: // LispValue value = f_lisp.NIL;
193: // boolean valid = true;
194: //
195: // try {
196: // value = parser.parse();
197: // if (DEBUG)
198: // System.err.println("Parser produced: " + value);
199: // }
200: // catch (EOFException ex)
201: // { // display a dialog?
202: // f_parent.message("\n*** Incomplete LISP Input - fix it and try again.\n");
203: // valid = false;
204: // }
205: //
206: // if (valid)
207: // {
208: // eval(value);
209: //
210: f_parent.checkForWindowSettingsChanges();
211: // }
212: }
213:
214: /**
215: * Input should be a LispString.
216: * @param input a LispString containing a command.
217: */
218: public void eval(LispValue input) {
219: f_parent.addHistoryItem(input.toString());
220:
221: f_parent.message(input, true);
222: f_parent.message(f_lisp.eval(input)); // result will be printed in the Output window.
223:
224: f_lastCommand = input.toString();
225:
226: // Select all the text in the input box
227: setText(f_lastCommand);
228: selectAll();
229: }
230:
231: // This is called when the user hits RETURN in the input window.
232: public void actionPerformed(ActionEvent e) {
233: if (DEBUG)
234: System.err.println("ActionPerformed: "
235: + e.getActionCommand());
236:
237: String command = e.getActionCommand();
238:
239: if (command.equals("comboBoxEdited") || command.equals("eval")) {
240: String inStr = f_inputArea.getText();
241: if (DEBUG)
242: System.err.println("Retrievd typed input: " + inStr);
243:
244: if (inStr.trim().equals(""))
245: return;
246:
247: if (DEBUG)
248: System.err.println("LispInput: got input: " + inStr);
249:
250: eval(inStr);
251: f_firstCharOfCommand = true;
252: }
253:
254: else if (e.getActionCommand().equals("largerArea"))
255: incrementEditorLines(1);
256:
257: else if (e.getActionCommand().equals("smallerArea"))
258: incrementEditorLines(-1);
259:
260: }
261:
262: public void incrementEditorLines(int increment) {
263: int numRows = f_inputArea.getRows() + increment;
264: if (numRows > 0) {
265: f_inputArea.setRows(numRows);
266: f_parent.redraw();
267:
268: if (DEBUG)
269: System.err.println("Listener input now has " + numRows
270: + " rows");
271:
272: f_inputArea.setText("(setq *LISTENER-INPUT-ROWS* "
273: + numRows + ")");
274: ActionEvent newEvent = new ActionEvent(f_inputArea,
275: ActionEvent.ACTION_PERFORMED, "comboBoxEdited");
276: actionPerformed(newEvent);
277: }
278: }
279:
280: public void keyPressed(KeyEvent e) {
281: }
282:
283: public void keyReleased(KeyEvent e) {
284: }
285:
286: /**
287: * Implements parenthesis matching
288: */
289: public void keyTyped(KeyEvent e) {
290: char key = e.getKeyChar();
291:
292: // System.err.println("LispInput.keyTyped: " + key + " (" + e.getKeyCode() +")");
293: if ((key == Character.LINE_SEPARATOR) && (e.isControlDown()))
294: // Toolkit.getDefaultToolkit().
295: // getSystemEventQueue().
296: // postEvent(new ActionEvent(e.getSource(),
297: // ActionEvent.ACTION_PERFORMED,
298: // "comboBoxEdited"));
299: actionPerformed(new ActionEvent(e.getSource(),
300: ActionEvent.ACTION_PERFORMED, "comboBoxEdited"));
301:
302: //int quoteCount = 0;
303: //int parenCount = 0;
304:
305: // Check for editing characters
306: //if (handleEmacsCommand(e))
307: // return;
308:
309: // Reset command multiplier
310: //f_commandMultiplier = 1;
311:
312: /*
313: // Interrupt the parenthesis matching if it's in progress
314: synchronized (myThread)
315: {
316: if (flashing)
317: {
318: myThread.interrupt();
319: }
320: }
321:
322: // PARENTHESIS MATCHING
323: if (key == ')')
324: {
325: input = f_comboBox.getSelectedItem().toString() + " ";
326:
327: // Count doublequotes - don't check if we are in a string
328: for (int i=0; i<input.length(); ++i)
329: if (input.charAt(i) == '"') ++ quoteCount;
330:
331: if ((quoteCount % 2) == 0) // Do a paren match
332: {
333: for (int i=input.length()-1; i>=0; --i)
334: if (input.charAt(i) == '"')
335: while (input.charAt(--i) != '"');
336: else
337: if (input.charAt(i) == ')')
338: ++parenCount;
339: else if (input.charAt(i) == '(')
340: {
341: --parenCount;
342: if (parenCount == 0) // Highlight the paren for an instant
343: {
344: matchingPosition = i;
345: // Determine whether the matchingPosition is visible
346: if ((f_comboBox.getCaretPosition() - matchingPosition) < this.getColumns())
347: {
348: synchronized(myThread) { myThread.notify(); }
349: }
350: }
351: }
352: }
353: }
354: */
355: }
356:
357: /**
358: * Returns true if no further event handling is necessary.
359: */
360: boolean handleEmacsCommand(KeyEvent e) {
361: //char key = e.getKeyChar();
362: //int type = e.getID();
363: // boolean delChar = true;
364:
365: return false;
366:
367: /*
368: // 29 Sep 2003 (mh)
369: // TextField no longer enters Emacs commands into the field
370: // so we don't need to delete the character when it is typed.
371:
372: // If they type an editing command as the first character of
373: // a command, the old command will be deleted, which we don't
374: // want. So we check for first characters and restore the text.
375:
376: if (key >= 27) // not a Control character
377: {
378: FirstCharOfCommand = false;
379: return false;
380: }
381:
382: if (FirstCharOfCommand)
383: {
384: f_comboBox.setSelectedItem(LastCommand);
385: FirstCharOfCommand = false;
386: // delChar = false;
387: }
388:
389: // EMACS bindings
390: if (key == 1) // CTRL-A, Beginning of Line
391: {
392: //if (delChar) eraseLastCharTyped(); // Stupid TextField doesn't let us filter characters.
393: /*
394: if (type == KeyEvent.KEY_TYPED)
395: this.setCaretPosition(0);
396: */
397: /*
398: CommandMultiplier = 1;
399: return true;
400: }
401:
402: if (key == 2) // CTRL-B BACKWARD one space
403: {
404: //if (delChar) eraseLastCharTyped(); // Stupid TextField doesn't let us filter characters.
405:
406: int p = getCaretPosition();
407:
408: for (int i=0; i<CommandMultiplier; ++i)
409: if (p > 0)
410: this.setCaretPosition(this.getCaretPosition() - 1);
411:
412: CommandMultiplier = 1;
413: return true;
414: }
415:
416: if (key == 4) // CTRL-D DELETE forward
417: {
418: //if (delChar) eraseLastCharTyped(); // Stupid TextField doesn't let us filter characters.
419:
420: for (int i=0; i<CommandMultiplier; ++i)
421: {
422: this.setCaretPosition(this.getCaretPosition() + 1);
423: eraseLastCharTyped(); // Deletes the previous character.
424: }
425:
426: CommandMultiplier = 1;
427: return true;
428: }
429:
430: if (key == 5) // CTRL-E, EOL
431: {
432: //if (delChar) eraseLastCharTyped(); // Stupid TextField doesn't let us filter characters.
433: if (type == KeyEvent.KEY_TYPED)
434: this.setCaretPosition(this.getText().length()+1);
435:
436: CommandMultiplier = 1;
437: return true;
438: }
439:
440: if (key == 6) // CTRL-F FORWARD one space
441: {
442: //if (delChar) eraseLastCharTyped(); // Stupid TextField doesn't let us filter characters.
443: for (int i=0; i<CommandMultiplier; ++i)
444: this.setCaretPosition(this.getCaretPosition() + 1);
445:
446: CommandMultiplier = 1;
447: return true;
448: }
449:
450: if (key == 11) // CTRL-K Kill to end of line
451: {
452: //if (delChar) eraseLastCharTyped(); // Stupid TextField doesn't let us filter characters.
453:
454: saveText(killToEndOfLine());
455:
456: CommandMultiplier = 1;
457: return true;
458: }
459:
460: if (key == 21) // CTRL-U Command repeat x4
461: {
462: //if (delChar) eraseLastCharTyped(); // Stupid TextField doesn't let us filter characters.
463:
464: CommandMultiplier *= 4;
465: return true;
466: }
467:
468: if (key == 25) // CTRL-Y Yank kill buffer
469: {
470: //if (delChar) eraseLastCharTyped(); // Stupid TextField doesn't let us filter characters.
471: for (int i=0; i<CommandMultiplier; ++i)
472: restoreText();
473:
474: CommandMultiplier = 1;
475: return true;
476: }
477:
478: return false;
479: */
480: }
481:
482: // Editing command - used to erase control characters.
483: void eraseLastCharTyped() {
484: /* String s = getText();
485: int p = getCaretPosition();
486: if (p > 0)
487: {
488: this.setText(s.substring(0, p-1) + s.substring(p));
489: this.setCaretPosition(p-1);
490: }
491: */
492: }
493:
494: // Editing - deletes string to end of line.
495: String killToEndOfLine() {
496: /*
497: String s = getText();
498: int p = getCaretPosition();
499: String killed;
500:
501: killed = s.substring(p);
502: this.setText(s.substring(0, p));
503:
504: return killed;
505: */
506: return "";
507: }
508:
509: // Editing - save deleted text in a buffer
510: void saveText(String s) {
511: f_saveBuffer = s;
512: }
513:
514: // Editing - restore deleted text from a buffer
515: void restoreText() {
516: /*
517: String s = getText();
518: int p = getCaretPosition();
519:
520: setText(s.substring(0, p) + SaveBuffer + s.substring(p));
521: setCaretPosition(p + SaveBuffer.length());
522: */
523: }
524:
525: public void run() {
526: int h, v; // Top left corner of character to be boxed.
527:
528: synchronized (f_myThread) {
529: while (true) {
530: try {
531: f_myThread.wait();
532: } // Wait to be notified to start flashing
533: catch (InterruptedException e) {
534: }
535:
536: // can't do this until we are visible.
537: if (fontInfo == null)
538: setFontInfo();
539:
540: f_flashing = true;
541: // draw a box around the matching character.
542: /* Fixed-width font
543: // The font is a fixed-width font.
544: // h = matchingPosition * fontWidth + hFudge;
545: */
546:
547: // Variable-width font
548: h = hFudge
549: + fontInfo.stringWidth(f_input.substring(0,
550: f_matchingPosition));
551: v = vFudge;
552:
553: f_myGraphics.setXORMode(Color.white);
554: f_myGraphics.fillRect(h, v, fontWidth, fontHeight);
555:
556: // Sleep for 0.5 sec or until user types another key
557: try {
558: f_myThread.wait(500L);
559: } catch (InterruptedException e) {
560: } finally {
561: f_flashing = false;
562: f_myGraphics.fillRect(h, v, fontWidth, fontHeight);
563: f_myGraphics.setPaintMode();
564: }
565: }
566: }
567: }
568:
569: public String getText() {
570: return f_inputArea.getText();
571: }
572:
573: public void setText(String newText) {
574: f_inputArea.setText(newText);
575: }
576:
577: public void selectAll() {
578: f_inputArea.selectAll();
579: //((JTextComponent)f_comboBox.getEditor().getEditorComponent()).selectAll();
580: }
581:
582: public int getColumns() {
583: return f_inputArea.getColumns();
584: }
585:
586: public void setColumns(int newValue) {
587: f_inputArea.setColumns(newValue);
588: }
589:
590: public void setRows(int newValue) {
591: f_inputArea.setRows(newValue);
592: }
593:
594: public int getRows() {
595: return f_inputArea.getRows();
596: }
597:
598: public void addGoodHighlight(int start, int end) {
599: addHighlight(start, end, f_goodPainter);
600: }
601:
602: public void addBadHighlight(int start, int end) {
603: addHighlight(start, end, f_badPainter);
604: }
605:
606: public void addHighlight(int start, int end,
607: Highlighter.HighlightPainter painter) {
608: Highlighter highlighter = f_inputArea.getHighlighter();
609:
610: synchronized (f_highlightLock) {
611: try {
612: f_highlights.add(highlighter.addHighlight(start, end,
613: painter));
614: } catch (BadLocationException ble) {
615: // ignore
616: }
617: }
618: }
619:
620: public void clearHighlights() {
621: Highlighter highlighter = f_inputArea.getHighlighter();
622:
623: if (highlighter != null) {
624: synchronized (f_highlightLock) {
625: for (Iterator iterator = f_highlights.iterator(); iterator
626: .hasNext();) {
627: Highlighter.Highlight highlight = (Highlighter.Highlight) iterator
628: .next();
629: highlighter.removeHighlight(highlight);
630: }
631: f_highlights.clear();
632: }
633: }
634: }
635:
636: }
637:
638: class BracketMatcher implements CaretListener {
639: protected LispInput f_input;
640:
641: public BracketMatcher(LispInput input) {
642: f_input = input;
643: }
644:
645: public void caretUpdate(CaretEvent e) {
646: JTextComponent source = (JTextComponent) e.getSource();
647: int pos = e.getDot();
648: Document doc = source.getDocument();
649: char key = ' ';
650:
651: f_input.clearHighlights();
652:
653: try {
654: key = doc.getText(e.getDot() - 1, 1).charAt(0);
655: } catch (BadLocationException ble) { // ?? when does this happen?
656: return;
657: }
658:
659: // PARENTHESIS MATCHING
660: if (key == ')') {
661: String input = f_input.getText();
662: int quoteCount = 0;
663: int parenCount = 0;
664:
665: // Count doublequotes - don't check if we are in a string
666: for (int i = 0; i < pos; ++i)
667: if (input.charAt(i) == '"')
668: ++quoteCount;
669:
670: if ((quoteCount % 2) != 0) // Don't do a paren match if we are inside a string
671: return;
672:
673: // Find the matching paren
674: for (int i = pos - 1; i >= 0; --i) {
675: if (input.charAt(i) == '"')
676: while (input.charAt(--i) != '"')
677: ;
678: else if (input.charAt(i) == ')')
679: ++parenCount;
680: else if (input.charAt(i) == '(') {
681: --parenCount;
682: if (parenCount == 0) // Highlight the paren for an instant
683: {
684: f_input.addGoodHighlight(i, i + 1);
685: f_input.addGoodHighlight(pos - 1, pos);
686: return;
687: }
688: }
689: }
690:
691: // If we get here, we didn't match anything.
692: f_input.addBadHighlight(pos - 1, pos);
693:
694: }
695: }
696: }
|