Initial import of ClientModules from the sourceforge CVS repository
[remote/remote-gui.git] / DIKUClientMainModule / old / gui / Console.java
blob3913c8513a0935f50847ea49e15d47ca78b61aea
1 package gui;
3 import java.awt.Color;
4 import java.awt.Dimension;
5 import java.awt.Font;
6 import java.awt.Insets;
7 import java.awt.Rectangle;
8 import java.awt.datatransfer.DataFlavor;
9 import java.awt.datatransfer.Transferable;
10 import java.awt.event.ActionEvent;
11 import java.awt.event.ActionListener;
12 import java.awt.event.KeyAdapter;
13 import java.awt.event.KeyEvent;
14 import java.io.IOException;
15 import java.io.InterruptedIOException;
16 import java.io.OutputStream;
17 import java.io.PrintStream;
18 import java.util.ArrayList;
19 import java.util.Iterator;
20 import java.util.LinkedList;
21 import java.util.List;
22 import javax.swing.JTextPane;
23 import javax.swing.SwingUtilities;
24 import javax.swing.Timer;
25 import javax.swing.text.*;
27 /**
28 * A TextArea that supports terminal like functionality.
29 * @author Tony Johnson (tonyj@slac.stanford.edu)
30 * @version $Id: Console.java,v 1.1 2006/07/12 14:59:23 esbenzeuthen Exp $
34 * TODO: Support insert/overstrike mode
35 * TODO: Cut should only be allowed in input area
36 * TODO: Limit size of document
37 * TODO: Make Ctrl^C work properly
38 * TODO: Add support for command completion
39 * TODO: Abilty to write images (a la BeanShell) -- maybe done JTextPane has insertIcon method
40 * TODO: Worry about key bindings (PageUp, PageDown etc)
41 * TODO: *Document uses too much memory when lots of output!
43 public class Console extends JTextPane
45 /**
48 private static final long serialVersionUID = -6492939159097843176L;
49 private final static String defaultPrompt = "> ";
50 private String currentPrompt;
51 private List history = new LinkedList();
52 private List list = new ArrayList();
53 private List queue = new LinkedList();
54 private ListEntry last;
55 private Object lock = new Object();
56 private PrintStream logStream;
57 private ConsoleInputStream theInputStream;
58 private SimpleAttributeSet defStyle;
59 private SimpleAttributeSet promptStyle;
61 // We maintain several queues in this code. They are:
62 // 1) The typeAhead buffer, stuff which has been typed while the program attached to the console
63 // is busy. As soon as an Return is entered the contents of the typeAhead buffer is moved to the
64 // pasteAhead buffer.
65 // 2) The pasteAhead buffer. Complete commands that have been entered, but not yet echoed to the
66 // console because the program attached to the console is busy.
67 // 3) The queue, commands that have been entered, echoed to the conole (if appropriate) but not
68 // yet read by the input stream.
70 // Stuff pasted while not waiting for input goes into the pasteAhead buffer
71 private StringBuffer pasteAhead = new StringBuffer();
73 // Stuff typed while not waiting for input goes into the typeAhead buffer
74 private StringBuffer typeAhead = new StringBuffer();
75 private MyTimer timer;
76 private boolean log;
77 private boolean waitingForInput = false; // True while inputStream waiting for input
78 private int historyPos = -1;
79 private int maxHistory = 100;
80 private int start; // Of input area!
81 private int startLastLine; // Start of last line (before prompt)
83 /** Create a new Console */
84 public Console()
86 setFont(new Font("Courier", Font.PLAIN, 12));
87 // setPreferredSize(new Dimension(300, 200));
88 CKeyListener l = new CKeyListener();
89 addKeyListener(l);
90 timer = new MyTimer(l);
91 setEditable(false);
93 defStyle = new SimpleAttributeSet()
95 /**
98 private static final long serialVersionUID = 1L;
100 public Object getAttribute(Object key)
102 if (key.equals(StyleConstants.Foreground))
104 return getForeground();
106 else return super.getAttribute(key);
109 promptStyle = new SimpleAttributeSet();
110 promptStyle.addAttribute(StyleConstants.Foreground, new Color(0, 153, 51));
113 /** Create an input stream for reading user input from the console.
114 * The input stream will use the default prompt.
115 * @return The newly created input stream
117 public ConsoleInputStream getInputStream()
119 return getInputStream(defaultPrompt);
122 /** Create an input stream for reading user input from the console.
123 * @param initialPrompt The prompt to use for user input
124 * @return The newly created input stream
126 public ConsoleInputStream getInputStream(String initialPrompt)
128 if (theInputStream == null) theInputStream = new CInputStream(initialPrompt);
129 return theInputStream;
132 /** Sets a stream to use for writing logging output.
133 * All input/output to the console will be logged to this
134 * output stream.
135 * @param out The output stream to use, or <CODE>null</CODE> to turn off logging.
137 public synchronized void setLogStream(OutputStream out)
139 log = !(out == null);
140 if (logStream != null) logStream.flush();
141 if (log) logStream = new PrintStream(out);
142 else logStream = null;
145 /** Get the current log stream
146 * @return The current log stream, or <CODE>null</CODE> if no current stream
148 public OutputStream getLogStream()
150 return logStream;
153 /** Temporarily disables/enables logging.
154 * @param log <CODE>true</CODE> to enable logging.
156 public synchronized void setLoggingEnabled(boolean log)
158 this.log = log && (logStream != null);
161 /** Test if logging is currently enabled
162 * @return <CODE>true</CODE> if logging enabled.
164 public boolean isLoggingEnabled()
166 return log;
169 /** Get an output stream for writing to the console.
170 * @param set The attributes for text created by this output stream, or <CODE>null</CODE> for the default
171 * attributes.
172 * @param autoShow If true the console will "pop to the front" when new output is written.
173 * @return The newly created output stream
175 public ConsoleOutputStream getOutputStream(AttributeSet set, boolean autoShow)
177 return new COutputStream(set, autoShow);
179 public ConsoleOutputStream getOutputStream(AttributeSet set)
181 return getOutputStream(set, false);
184 /** Adds a listener for CTRL^C events
185 * @param l The listener to add.
187 public void addInterruptListener(ActionListener l)
189 listenerList.add(ActionListener.class, l);
191 /** Cleans up resources associated with this console. Closes any input streams
192 * associated with this console.
194 public void dispose()
196 setLogStream(null);
197 if (theInputStream != null)
201 synchronized (lock)
203 theInputStream.close();
204 theInputStream = null;
205 lock.notify();
208 catch (IOException x)
214 * Insert text (command(s)) as if it were typed by the user.
215 * The text will be sent to the Console's input stream. Any text already
216 * typed by the user will not be included in the text sent.
217 * @param text text to send
219 public void insertTextAsIfTypedByUser(String text)
221 if (!isEditable()) throw new RuntimeException("Cannot send text to non-editable console");
222 if (!text.endsWith("\n")) text += "\n";
223 if (waitingForInput)
227 int pos = text.indexOf('\n');
228 String firstLine = text.substring(0, pos);
229 // Take anything currently on the input line and move it to the end of the pasteAhead buffer
230 String originalText = lastLine();
231 Document doc = getDocument();
232 doc.remove(start, doc.getLength() - start);
233 doc.insertString(start, firstLine + "\n", null);
234 sendToInputStream(firstLine);
235 pasteAhead.append(text.substring(pos+1));
236 pasteAhead.append(originalText);
238 catch (Throwable x)
240 x.printStackTrace(); // Should never happen
243 else
245 pasteAhead.append(text);
246 if (!text.endsWith("\n")) pasteAhead.append('\n');
249 public void paste()
251 if (!isEditable())
253 getToolkit().beep();
255 else
259 Transferable t = getToolkit().getSystemClipboard().getContents(this);
260 String text = (String) t.getTransferData(DataFlavor.stringFlavor);
261 text = removeIllegalCharacters(text);
262 if (waitingForInput)
264 int pos = text.indexOf('\n');
265 boolean containsEOL = pos >= 0;
266 String firstLine = containsEOL ? text.substring(0, pos) : text;
268 // If there is a current selection _in the editable area_ delete it
269 int sStart = Math.max(start,getSelectionStart());
270 int sEnd = Math.max(start,getSelectionEnd());
271 if (sEnd-sStart > 0) getDocument().remove(sStart,sEnd-sStart);
272 // Add first line of pasted text to any current added text, at the insertion point
273 int cPos = getCaretPosition();
275 // If there is a new line in the pasted text, queue the whole line.
276 if (containsEOL)
278 getDocument().insertString(cPos, firstLine + "\n", null);
279 sendToInputStream(firstLine);
280 pasteAhead.append(text.substring(pos + 1));
282 else
284 getDocument().insertString(cPos, firstLine, null);
287 else
289 pasteAhead.append(text);
292 catch (Throwable x)
294 getToolkit().beep();
298 private String removeIllegalCharacters(String in)
300 StringBuffer out = null;
301 int l = in.length();
302 for (int i=0; i<l; i++)
304 char c = in.charAt(i);
305 if (c < ' ' && c != '\n')
307 if (out == null)
309 out = new StringBuffer(in.substring(0,i));
312 else if (out != null) out.append(c);
314 if (out == null) return in;
315 else
317 return out.toString();
320 /**
321 * A method to be called to request that the console be closed. Typically called by
322 * scripting engines when the user types "quit" or the equivalent. This close method
323 * does nothing, but is designed to be overriden by subclasses.
325 public void close()
328 /** Remove a listener for CTRL^C events.
329 * @param l The listener to remove
331 public void removeInterruptListener(ActionListener l)
333 listenerList.remove(ActionListener.class, l);
336 /** Called when CTRL^C is detected. Calls all the registered listeners. */
337 protected void fireInterruptAction()
339 int count = listenerList.getListenerCount(ActionListener.class);
340 if (count > 0)
342 ActionEvent event = new ActionEvent(this, ActionEvent.ACTION_FIRST, "Break");
343 ActionListener[] listeners = (ActionListener[]) listenerList.getListeners(ActionListener.class);
344 for (int i = listeners.length; i-- > 0;)
346 listeners[i].actionPerformed(event);
351 * Clears any output from the Console
353 public void clear()
357 Document d = getDocument();
358 d.remove(0, d.getLength());
359 if (waitingForInput)
361 synchronized (lock)
363 lock.notify();
367 catch (BadLocationException x)
372 private void setLastLine(String line)
376 int end = getDocument().getLength();
377 if (end > start)
379 getDocument().remove(start, end - start);
381 getDocument().insertString(start, line, null);
383 catch (BadLocationException x)
385 x.printStackTrace();
389 private void consume(KeyEvent event)
391 event.consume();
392 getToolkit().beep();
395 private void prepareToFlush()
397 if (waitingForInput)
401 // Take anything currently on the input line and move it to the typeAhead buffer
402 typeAhead.append(lastLine());
403 Document doc = getDocument();
404 doc.remove(startLastLine, doc.getLength() - startLastLine);
406 // TODO: Fixme, what if temporary prompt was set! Plus we dont want to add the
407 // initial entry again, as it was already put into the typeahead.
408 writePrompt(theInputStream.getCurrentPrompt(), null);
410 catch (Throwable x)
412 x.printStackTrace(); // Should never happen
415 else flush();
417 private void flush()
421 List old = list;
422 synchronized (this)
424 list = new ArrayList();
425 last = null;
428 Document doc = getDocument();
429 for (Iterator i = old.iterator(); i.hasNext();)
431 ListEntry entry = (ListEntry) i.next();
432 doc.insertString(doc.getLength(), entry.string.toString(), entry.set);
434 setCaretPosition(doc.getLength());
436 catch (BadLocationException x)
441 private boolean inLastLine()
443 int caret = getCaretPosition();
444 return caret >= start;
447 private String lastLine()
451 Document doc = getDocument();
452 return doc.getText(start, doc.getLength() - start);
454 catch (BadLocationException x)
456 return null;
460 private void sendToInputStream(String line)
462 if (log) logStream.println(currentPrompt+line);
463 synchronized (lock)
465 boolean wasEmpty = queue.isEmpty();
466 queue.add(line + "\n");
467 if (line.length() > 0)
469 history.add(line);
471 while (history.size() >= maxHistory) history.remove(0);
472 historyPos = history.size();
473 if (wasEmpty)
475 lock.notify();
476 waitingForInput = false;
480 // Overriden to make sure text entered at the keyboard doesnt inherit the prompt color
481 public MutableAttributeSet getInputAttributes() {
482 return defStyle;
484 private void writeMessage(String line, AttributeSet style)
488 Document doc = getDocument();
489 doc.insertString(doc.getLength(), line, style == null ? defStyle : style);
490 setCaretPosition(doc.getLength());
492 catch (BadLocationException x)
496 // This method can be called on any thread, so rather than directly updating the
497 // console data is added to a queue (list).
498 private void writeOutput(String s, AttributeSet set) throws IOException
500 synchronized (this)
502 boolean wasEmpty = list.isEmpty();
503 // If the attributes are the same as last time, simply append to
504 // existing ListEntry, otherwise create a new entry.
505 if ((last != null) && (set == last.set))
507 last.string.append(s);
509 else
511 list.add(last = new ListEntry(s, set));
513 if (wasEmpty)
515 timer.startLater();
520 private void writePrompt(String prompt, String initialEntry)
522 flush(); // Flush any output still to be sent to the console
523 startLastLine = getDocument().getLength();
526 Rectangle r = modelToView(startLastLine);
527 Insets insets = this.getInsets();
528 if (r.x > insets.left)
530 writeMessage("\n", defStyle);
531 startLastLine = getDocument().getLength();
534 catch (BadLocationException x) {} // Can't happen
535 currentPrompt = prompt;
536 if (prompt != null)
538 writeMessage(prompt, promptStyle);
540 start = getDocument().getLength();
542 if (pasteAhead.length() > 0)
544 int pos = pasteAhead.indexOf("\n");
545 boolean hasEOL = pos >= 0;
546 if (hasEOL)
548 String ta = pasteAhead.substring(0, pos);
549 writeMessage(ta + "\n", null);
550 pasteAhead.delete(0, pos + 1);
552 String text = lastLine();
553 setCaretPosition(getDocument().getLength());
554 text = text.substring(0, text.length() - 1); // trim \n
555 sendToInputStream(text);
556 return; // without setting waitingForInput to true
558 else
560 writeMessage(pasteAhead.toString(), null);
561 pasteAhead.setLength(0);
562 if (typeAhead.length() > 0)
564 writeMessage(typeAhead.toString(), null);
565 typeAhead.setLength(0);
569 else
571 if (initialEntry != null)
573 writeMessage(initialEntry, null);
575 if (typeAhead.length() > 0)
577 writeMessage(typeAhead.toString(), null);
578 typeAhead.setLength(0);
581 setEditable(true);
582 // Normally the cursor is automatically made visible when the focus is
583 // gained. However if setEditable(true) is done when we already have the
584 // focus then this doesn't work, so...
585 if (!getCaret().isVisible() && hasFocus()) getCaret().setVisible(true);
586 waitingForInput = true;
589 /** Getter for property promptColor.
590 * @return Value of property promptColor.
593 public Color getPromptColor()
595 return (Color) promptStyle.getAttribute(StyleConstants.Foreground);
598 /** Setter for property promptColor.
599 * @param promptColor New value of property promptColor.
602 public void setPromptColor(Color promptColor)
604 promptStyle.addAttribute(StyleConstants.Foreground, promptColor);
607 public void addNotify()
609 super.addNotify();
610 requestFocusInWindow();
613 private class CInputStream extends ConsoleInputStream implements Runnable
615 int pos;
616 private byte[] buffer;
618 CInputStream(String prompt)
620 setPrompt(prompt);
623 public int read() throws IOException
625 if ((buffer == null) || (pos >= buffer.length))
627 if (!fillBuffer()) return -1;
630 return buffer[pos++];
633 public int read(byte[] b, int off, int len) throws IOException
635 if ((buffer == null) || (pos >= buffer.length))
637 if (!fillBuffer()) return -1;
640 int l = Math.min(len, buffer.length - pos);
641 System.arraycopy(buffer, pos, b, off, l);
642 pos += l;
644 return l;
647 public void run()
649 writePrompt(getCurrentPrompt(), getInitialEntry());
651 // Called by the input stream when it needs more input
652 private boolean fillBuffer() throws IOException
654 synchronized (lock)
656 while (queue.isEmpty())
660 SwingUtilities.invokeLater(this);
661 lock.wait();
662 // In case someone disposed of the console while we were waiting
663 if (theInputStream == null) return false;
665 catch (InterruptedException x)
667 throw new InterruptedIOException();
671 String line = queue.remove(0).toString();
672 buffer = line.getBytes("ISO-8859-1");
673 pos = 0;
675 return true;
679 private class CKeyListener extends KeyAdapter implements ActionListener
681 public void keyPressed(KeyEvent keyEvent)
683 if ((keyEvent.getKeyCode() == KeyEvent.VK_C) && ((keyEvent.getModifiers() & keyEvent.CTRL_MASK) != 0))
685 fireInterruptAction();
687 else if (keyEvent.getKeyCode() == KeyEvent.VK_ENTER)
689 if (!waitingForInput)
691 keyEvent.consume();
693 else
695 setCaretPosition(getDocument().getLength());
698 else if (!inLastLine())
700 keyEvent.consume(); // No Beep
702 else if (keyEvent.getKeyCode() == keyEvent.VK_LEFT)
704 if (getCaretPosition() == start)
706 consume(keyEvent);
709 else if (keyEvent.getKeyCode() == KeyEvent.VK_UP)
711 if (historyPos <= 0)
713 consume(keyEvent);
715 else
717 String line = history.get(--historyPos).toString();
718 setLastLine(line);
719 keyEvent.consume();
722 else if (keyEvent.getKeyCode() == KeyEvent.VK_DOWN)
724 if (historyPos >= (history.size() - 1))
726 consume(keyEvent);
728 else
730 String line = history.get(++historyPos).toString();
731 setLastLine(line);
732 keyEvent.consume();
737 public void keyTyped(KeyEvent keyEvent)
739 if (waitingForInput)
741 if (!inLastLine())
743 setCaretPosition(getDocument().getLength());
745 else
747 // if the selection spans more than the current line (for instance the prompt)
748 // Then we need to remove the extra region
749 Caret caret = getCaret();
750 if (caret.getMark() < start)
752 int pos = caret.getDot();
753 caret.setDot(start);
754 caret.moveDot(pos);
757 if (keyEvent.getKeyChar() == KeyEvent.VK_BACK_SPACE)
759 int cPos = getCaretPosition();
760 if (cPos == start)
762 consume(keyEvent);
765 else if (keyEvent.getKeyChar() == keyEvent.VK_ENTER)
767 theInputStream.clearOneTimePrompt();
768 String text = lastLine();
769 setCaretPosition(getDocument().getLength());
770 text = text.substring(0, text.length() - 1); // trim \n
771 sendToInputStream(text);
774 else
776 if (keyEvent.getKeyChar() == KeyEvent.VK_BACK_SPACE)
778 int l = typeAhead.length();
779 if (l > 0)
781 typeAhead.deleteCharAt(l - 1);
784 else if (keyEvent.getKeyChar() == keyEvent.VK_ENTER)
786 // Move the contents of the typeAhead buffer to the pasteAhead buffer
787 pasteAhead.append(typeAhead);
788 pasteAhead.append('\n');
789 typeAhead.setLength(0);
791 else if ((keyEvent.getModifiers() & keyEvent.CTRL_MASK) == 0)
793 typeAhead.append(keyEvent.getKeyChar());
795 keyEvent.consume();
798 // called (on event thread), when new output to be written to console.
799 public void actionPerformed(ActionEvent e)
801 // If there is stuff which still needs to be flushed, then flush it
802 if (!list.isEmpty()) prepareToFlush();
806 class COutputStream extends ConsoleOutputStream
808 COutputStream(AttributeSet set, boolean autoRun)
810 super(set,autoRun);
813 public void write(byte[] b, int off, int len, AttributeSet set) throws IOException
815 if (log) logStream.write(b,off,len);
816 writeOutput(new String(b, off, len), set);
820 private static class ListEntry
822 AttributeSet set;
823 StringBuffer string;
825 ListEntry(String string, AttributeSet set)
827 this.string = new StringBuffer(string);
828 this.set = set;
831 private static class MyTimer extends Timer implements Runnable
833 MyTimer(ActionListener l)
835 super(10, l);
836 setRepeats(false);
838 public void run()
840 start();
842 void startLater()
844 SwingUtilities.invokeLater(this);