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.*; /** * TStdinPane is a JPanel which acts as an editor for the stdin of a TVirtualMachine. * * @author btsang * @version 7.1 */ public class TStdinPane extends JPanel { private static final String CLASS_STRING = TStdinPane.class.toString(); /** * The command to add the contents of the inputField to the stdin stream of the virtualMachine. */ public static final String ADD_COMMAND = CLASS_STRING + "#addCommand"; /** * The command to remove the selected items from the stdin stream of the virtualMachine. */ public static final String REMOVE_COMMAND = CLASS_STRING + "#removeCommand"; /** * 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"; private PTextField inputField; private ListModel consumedListModel; private ListModel unconsumedListModel; private ListSelectionModel unconsumedSelectionModel; private TWordBuffer oldConsumedStdin, newConsumedStdin; private TWordBuffer oldUnconsumedStdin, newUnconsumedStdin; private JButton addButton, removeButton; private PHyperlink openLink, saveAsLink; private TVirtualMachine virtualMachine; private Runner runner; private Listener listener; /** * Creates a new TStdinPane. */ public TStdinPane(TVirtualMachine virtualMachine, Action openStdinAction, Action saveStdinAction, boolean allowBlink) { super(new GridBagLayout(), true); if (virtualMachine == null) throw new NullPointerException(); this.virtualMachine = virtualMachine; oldConsumedStdin = new TWordBuffer(); newConsumedStdin = new TWordBuffer(); oldUnconsumedStdin = new TWordBuffer(); newUnconsumedStdin = new TWordBuffer(); if (allowBlink) runner = new Runner(); listener = new Listener(); virtualMachine.addChangeListener(listener); inputField = new PTextField(16); inputField.setFont(null); inputField.addActionListener(listener); inputField.setActionCommand(ADD_COMMAND); if (allowBlink) inputField.addMouseListener(listener); add( inputField, new GridBagConstraints( 0, 0, 3, 1, 1.0, 0.0, GridBagConstraints.NORTHWEST, GridBagConstraints.BOTH, new Insets(2, 2, 2, 2), 0, 0 ) ); addButton = new JButton("Add"); addButton.setActionCommand(ADD_COMMAND); addButton.addActionListener(listener); add( addButton, new GridBagConstraints( 3, 0, 1, 1, 0.0, 0.0, GridBagConstraints.NORTHWEST, GridBagConstraints.HORIZONTAL, new Insets(2, 2, 2, 2), 0, 0 ) ); add( new JLabel("Stdin"), new GridBagConstraints( 0, 1, 1, 1, 0.0, 0.0, GridBagConstraints.SOUTHWEST, GridBagConstraints.NONE, new Insets(2, 2, 2, 2), 0, 0 ) ); openLink = new PHyperlink(openStdinAction, PHyperlink.METAL_STYLE); openLink.setText("Open..."); add( openLink, new GridBagConstraints( 1, 1, 1, 1, 1.0, 0.0, GridBagConstraints.SOUTHEAST, GridBagConstraints.NONE, new Insets(2, 2, 2, 2), 0, 0 ) ); saveAsLink = new PHyperlink(saveStdinAction, PHyperlink.METAL_STYLE); saveAsLink.setText("Save As..."); add( saveAsLink, new GridBagConstraints( 2, 1, 1, 1, 0.0, 0.0, GridBagConstraints.SOUTHEAST, GridBagConstraints.NONE, new Insets(2, 2, 2, 2), 0, 0 ) ); removeButton = new JButton("Remove"); removeButton.setActionCommand(REMOVE_COMMAND); removeButton.addActionListener(listener); add( removeButton, new GridBagConstraints( 3, 1, 1, 1, 0.0, 0.0, GridBagConstraints.NORTHWEST, GridBagConstraints.HORIZONTAL, new Insets(2, 2, 2, 2), 0, 0 ) ); { // Create a concatenation of lists PScrollablePanel listPanel = new PScrollablePanel(new GridBagLayout()); listPanel.setFont(null); { // Create the consumed lists consumedListModel = new ListModel(); PList list = new PList(consumedListModel); list.setEnabled(false); list.setFont(null); listPanel.add( list, new GridBagConstraints( 0, 0, 1, 1, 1.0, 0.0, GridBagConstraints.NORTHWEST, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0 ) ); } { // Create the unconsumed lists unconsumedListModel = new ListModel(); PList list = new PList(unconsumedListModel); list.setFont(null); unconsumedSelectionModel = list.getSelectionModel(); listPanel.add( list, new GridBagConstraints( 0, 1, 1, 1, 1.0, 0.0, GridBagConstraints.NORTHWEST, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0 ) ); } { // Create the (Prompt) list Object data[] = new Object[1]; data[0] = "(Prompt)"; PList list = new PList(data); list.setEnabled(false); list.setFont(null); list.setCellRenderer(new EnabledListCellRenderer()); listPanel.add( list, new GridBagConstraints( 0, 2, 1, 1, 1.0, 1.0, GridBagConstraints.NORTHWEST, GridBagConstraints.BOTH, new Insets(0, 0, 0, 0), 0, 0 ) ); } listPanel.setBackground(Color.white); JScrollPane scrollPane = new JScrollPane( listPanel, JScrollPane.VERTICAL_SCROLLBAR_ALWAYS, JScrollPane.HORIZONTAL_SCROLLBAR_AS_NEEDED ); scrollPane.setFont(null); scrollPane.getViewport().setFont(null); add( scrollPane, new GridBagConstraints( 0, 2, 4, 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 TStdinPane. * * @param virtualMachine The virtualMachine to be attached to this TStdinPane. */ 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 TStdinPane. * * @return The virtualMachine attached to this TStdinPane. */ 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 == ADD_COMMAND) { virtualMachine.getUnconsumedStdin(newUnconsumedStdin); String string = inputField.getText(); int length = string.length(); inputField.setText(""); int charCtr = 0; while (charCtr < length) { char ch = string.charAt(charCtr); if (TWord.isHexDigit(ch)) { int peekCtr = charCtr + 1; while (peekCtr < length && peekCtr < charCtr + 4 && TWord.isHexDigit(string.charAt(peekCtr))) peekCtr++; try { newUnconsumedStdin.add( TWord.parseWord( string.substring(charCtr, peekCtr), 16 ) ); } catch (NumberFormatException e) { e.printStackTrace(); } charCtr = peekCtr; } else { charCtr++; } } unconsumedSelectionModel.clearSelection(); virtualMachine.setStdin(null, newUnconsumedStdin); return true; } //////////////////////////////////////////////////////////////////////////////////////////// if (command == REMOVE_COMMAND) { if (unconsumedSelectionModel.isSelectionEmpty()) return true; newUnconsumedStdin.clear(); int size = oldUnconsumedStdin.getSize(); for (int ctr = 0; ctr < size; ctr++) { if (!unconsumedSelectionModel.isSelectedIndex(ctr)) newUnconsumedStdin.add(oldUnconsumedStdin.getWord(ctr)); } unconsumedSelectionModel.clearSelection(); virtualMachine.setStdin(null, newUnconsumedStdin); return true; } //////////////////////////////////////////////////////////////////////////////////////////// if (command == UPDATE_COMMAND) { virtualMachine.getConsumedStdin(newConsumedStdin); virtualMachine.getUnconsumedStdin(newUnconsumedStdin); if (!newConsumedStdin.equals(oldConsumedStdin)) { consumedListModel.setData(newConsumedStdin); } if (!newUnconsumedStdin.equals(oldUnconsumedStdin)) { unconsumedListModel.setData(newUnconsumedStdin); } TWordBuffer temp = oldConsumedStdin; oldConsumedStdin = newConsumedStdin; newConsumedStdin = temp; temp = oldUnconsumedStdin; oldUnconsumedStdin = newUnconsumedStdin; newUnconsumedStdin = temp; if (runner != null && virtualMachine.needsInput()) runner.start(); boolean isRunning = virtualMachine.isRunning(); addButton.setEnabled(!isRunning); removeButton.setEnabled(!isRunning); inputField.setEnabled(!isRunning); 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 stdin text field from blinking if the user clicks on it * and listens to action events from JButtons and PTextFields. * * @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 inputField. */ public void mouseClicked(MouseEvent e) { } /** * Implement MouseListener to stop the runner if the user clicks on the inputField. */ public void mousePressed(MouseEvent e) { runner.stop(); } /** * Implement MouseListener to stop the runner if the user clicks on the inputField. */ public void mouseReleased(MouseEvent e) { } /** * Implement MouseListener to stop the runner if the user clicks on the inputField. */ public void mouseEntered(MouseEvent e) { } /** * Implement MouseListener to stop the runner if the user clicks on the inputField. */ public void mouseExited(MouseEvent e) { } } /** * The Runner of a TStdin blinks the inputField when the Virtual machine needs input. 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 inputField. */ public void run() { if (Thread.currentThread() != runningThread) throw new IllegalStateException(); boolean done = false; boolean colorBackground = false; Color originalBackground = inputField.getBackground(); synchronized (this) { if (count >= 3) { done = true; inputField.setBackground(originalBackground); inputField.repaint(); runningThread = null; } } while (!done) { colorBackground = !colorBackground; if (colorBackground) inputField.setBackground(inputField.getSelectionColor()); else inputField.setBackground(originalBackground); inputField.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; inputField.setBackground(originalBackground); inputField.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); } } } }