package edu.princeton.toy; import java.awt.*; import java.awt.event.*; import java.io.*; import javax.swing.*; import javax.swing.event.*; import javax.swing.text.*; import edu.princeton.swing.*; import edu.princeton.toy.lang.*; /** * TStdoutPane is a JPanel which acts as an editor for the stdin of a TVirtualMachine. * * @author btsang * @version 7.1 */ public class TStdoutPane extends JPanel { private static final String CLASS_STRING = TStdoutPane.class.toString(); /** * The command to update the contents of the inputList with the stdin stream of the * virtualMachine. */ public static final String UPDATE_COMMAND = CLASS_STRING + "#updateCommand"; /** * The command to call up the Exception Handling pane of the TOptionsFrame. */ public static final String CHANGE_HANDLER_COMMAND = CLASS_STRING + "#changeHandlerCommand"; private JPanel stderrPanel; private PTextArea stderrTextArea; private ListModel listModel; private TWordBuffer oldStdout, newStdout; private PHyperlink saveAsLink; private TVirtualMachine virtualMachine; private Listener listener; private Runner runner; /** * Creates a new TStdoutPane. */ public TStdoutPane(TVirtualMachine virtualMachine, Action saveStdoutAction) { super(new GridBagLayout(), true); if (virtualMachine == null) throw new NullPointerException(); this.virtualMachine = virtualMachine; oldStdout = new TWordBuffer(); newStdout = new TWordBuffer(); runner = new Runner(); listener = new Listener(); virtualMachine.addChangeListener(listener); { // Prepare the stderr panel stderrPanel = new JPanel(new GridBagLayout()); stderrPanel.add( new JLabel("Stderr"), new GridBagConstraints( 0, 0, 1, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(0, 0, 2, 2), 0, 0 ) ); PHyperlink hyperlink = new PHyperlink( "Change Settings...", null, PHyperlink.METAL_STYLE ); hyperlink.setActionCommand(CHANGE_HANDLER_COMMAND); hyperlink.addActionListener(listener); stderrPanel.add( hyperlink, new GridBagConstraints( 1, 0, 1, 1, 1.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(0, 2, 2, 0), 0, 0 ) ); stderrTextArea = new PTextArea(); stderrTextArea.setFont(null); stderrTextArea.setLineWrap(true); stderrTextArea.setWrapStyleWord(true); stderrTextArea.setEditable(false); stderrTextArea.addMouseListener(listener); JScrollPane scrollPane = new JScrollPane( stderrTextArea, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_NEVER ); scrollPane.setFont(null); scrollPane.getViewport().setFont(null); scrollPane.setMinimumSize(new Dimension(100, 100)); stderrPanel.add( scrollPane, new GridBagConstraints( 0, 1, 2, 1, 1.0, 1.0, GridBagConstraints.NORTHWEST, GridBagConstraints.BOTH, new Insets(2, 0, 0, 0), 0, 0 ) ); stderrPanel.setFont(null); stderrPanel.setVisible(false); add( stderrPanel, new GridBagConstraints( 0, 0, 2, 1, 1.0, 0.0, GridBagConstraints.NORTHWEST, GridBagConstraints.BOTH, new Insets(2, 2, 2, 2), 0, 0 ) ); } add( new JLabel("Stdout"), new GridBagConstraints( 0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.WEST, GridBagConstraints.NONE, new Insets(2, 2, 2, 2), 0, 0 ) ); saveAsLink = new PHyperlink(saveStdoutAction, PHyperlink.METAL_STYLE); saveAsLink.setText("Save As..."); add( saveAsLink, new GridBagConstraints( 1, 1, 1, 1, 1.0, 0.0, GridBagConstraints.EAST, GridBagConstraints.NONE, new Insets(2, 2, 2, 2), 0, 0 ) ); { // Create the stdout list listModel = new ListModel(); PList list = new PList(listModel); list.setFont(null); JScrollPane scrollPane = new JScrollPane( list, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED ); scrollPane.setFont(null); scrollPane.getViewport().setFont(null); add( scrollPane, new GridBagConstraints( 0, 2, 2, 1, 1.0, 1.0, GridBagConstraints.NORTHWEST, GridBagConstraints.BOTH, new Insets(2, 2, 2, 2), 0, 0 ) ); } doCommand(UPDATE_COMMAND, null); } /** * Sets the virtualMachine attached to this TStdoutPane. * * @param virtualMachine The virtualMachine to be attached to this TStdoutPane. */ public void setVirtualMachine(TVirtualMachine virtualMachine) { if (virtualMachine == null) throw new NullPointerException(); if (this.virtualMachine == virtualMachine) return; this.virtualMachine.removeChangeListener(listener); virtualMachine.addChangeListener(listener); this.virtualMachine = virtualMachine; doCommand(UPDATE_COMMAND, null); } /** * Returns the virtualMachine attached to this TStdoutPane. * * @return The virtualMachine attached to this TStdoutPane. */ public TVirtualMachine getVirtualMachine() { return virtualMachine; } /** * Performs a command based on the argument. * * @param command A string representing the command. Note that pointer equality (not string * equality) is tested here, so it is important to use the string constants defined in this * class. An IllegalArgumentException will be thrown if the argument is invalid. * @return True iff the command was executed sucessfully. */ public synchronized boolean doCommand(String command, Object extraInfo) { //////////////////////////////////////////////////////////////////////////////////////////// if (command == UPDATE_COMMAND) { virtualMachine.getStdout(newStdout); if (!newStdout.equals(oldStdout)) listModel.setData(newStdout); TWordBuffer temp = oldStdout; oldStdout = newStdout; newStdout = temp; boolean hasEncounteredError = virtualMachine.hasEncounteredError(); if (stderrPanel.isVisible() != hasEncounteredError) { if (hasEncounteredError) { stderrTextArea.setText(virtualMachine.getStderr()); stderrPanel.setVisible(true); revalidate(); runner.start(); } else { stderrPanel.setVisible(false); stderrTextArea.setText(""); revalidate(); } } return true; } //////////////////////////////////////////////////////////////////////////////////////////// if (command == CHANGE_HANDLER_COMMAND) { TOptionsFrame.show(TOptionsFrame.EXECUTION_EXCEPTION_NODE); return true; } throw new IllegalArgumentException(); } /** * The Listener of a TStdoutPane pays attention to the changes in the virtualMachine and updates * the components, it also stops the stderr text area from blinking if the user clicks on it * and listens to action events from hyperlinks. * * @author btsang * @version 7.1 */ protected class Listener implements ActionListener, ChangeListener, MouseListener { /** * Implement ActionListener to pass commands to the doCommand() method. */ public void actionPerformed(ActionEvent e) { doCommand(e.getActionCommand(), null); } /** * Implement ChangeListener to update the list whenever the virtual machine's state changes. */ public void stateChanged(ChangeEvent e) { doCommand(UPDATE_COMMAND, null); } /** * Implement MouseListener to stop the runner if the user clicks on the stderrTextArea. */ public void mouseClicked(MouseEvent e) { } /** * Implement MouseListener to stop the runner if the user clicks on the stderrTextArea. */ public void mousePressed(MouseEvent e) { runner.stop(); } /** * Implement MouseListener to stop the runner if the user clicks on the stderrTextArea. */ public void mouseReleased(MouseEvent e) { } /** * Implement MouseListener to stop the runner if the user clicks on the stderrTextArea. */ public void mouseEntered(MouseEvent e) { } /** * Implement MouseListener to stop the runner if the user clicks on the stderrTextArea. */ public void mouseExited(MouseEvent e) { } } /** * The Runner of a TStdoutPane blinks the stderrTextArea when an error occurs. This class is * completely thread-safe in that the user need not worry about the current status of the * Runner when he calls start() or stop(). * * @author btsang * @version 7.1 */ protected class Runner implements Runnable { int count; Thread runningThread; /** * Creates a new Runner. */ protected Runner() { } /** * Starts the blink thread if it hasn't started, or tells it to keep on going if it has. */ protected synchronized void start() { if (runningThread == null) { count = 0; runningThread = new Thread(this); runningThread.start(); } else { count = 0; } } /** * Stops the blink thread at the next available opportunity, does nothing if the thread * hasn't started. */ protected synchronized void stop() { count = Integer.MAX_VALUE - 1; } /** * Implement runnable to blink the stderrTextArea. */ public void run() { if (Thread.currentThread() != runningThread) throw new IllegalStateException(); boolean done = false; boolean colorBackground = false; Color originalBackground = stderrTextArea.getBackground(); synchronized (this) { if (count >= 3) { done = true; stderrTextArea.setBackground(originalBackground); stderrTextArea.repaint(); runningThread = null; } } while (!done) { colorBackground = !colorBackground; if (colorBackground) stderrTextArea.setBackground(stderrTextArea.getSelectionColor()); else stderrTextArea.setBackground(originalBackground); stderrTextArea.repaint(); try { Thread.sleep(300); if (colorBackground) count++; } catch (InterruptedException e) { e.printStackTrace(); count = Integer.MAX_VALUE - 1; } synchronized (this) { if (count >= 3) { done = true; stderrTextArea.setBackground(originalBackground); stderrTextArea.repaint(); runningThread = null; } } } } } /** * A customized implementation of ListModel meant specifically for displaying stdin. * * @author btsang * @version 7.1 */ protected static class ListModel extends AbstractListModel { private String data[]; private int size; /** * Constructs a new ListModel. */ public ListModel() { data = new String[100]; } public Object getElementAt(int index) { if (index < 0 || index >= size) throw new ArrayIndexOutOfBoundsException(); return data[index]; } /** * Returns the number of elements in the model. * * @return The number of elements in the model. */ public int getSize() { return size; } /** * Updates the ListModel's data to conform with the provided TWordBuffer. * * @param buffer A TWordBuffer containing the new data. */ protected void setData(TWordBuffer buffer) { int oldSize = size; int newSize = buffer.getSize(); size = newSize; if (data.length < newSize) { data = new String[newSize * 2]; } for (int ctr = 0; ctr < newSize; ctr++) data[ctr] = buffer.getWord(ctr).toString(false); // Fire the correct change events to announce that everything has changed. if (oldSize < newSize) { fireIntervalAdded(this, oldSize, newSize - 1); if (oldSize > 0) fireContentsChanged(this, 0, oldSize - 1); } else if (oldSize == newSize) { if (newSize > 0) fireContentsChanged(this, 0, newSize - 1); } else { fireIntervalRemoved(this, newSize, oldSize - 1); if (newSize > 0) fireContentsChanged(this, 0, newSize - 1); } } } }