package edu.princeton.toy; import java.awt.*; import java.awt.event.*; import javax.swing.*; import javax.swing.border.*; import javax.swing.event.*; import edu.princeton.swing.*; import edu.princeton.toy.lang.*; /** * TSimStdout2Pane is a variant of TSimStdoutPane which displays its data in the form of a set of * punchcards. * * @author btsang * @version 7.1 */ public class TSimStdout2Pane extends JPanel implements Scrollable { private static final String CLASS_STRING = TSimStdout2Pane.class.toString(); /** * The command to update the contents of the inputList with the stdin stream of the * virtualMachine. */ public static final String RESCALE_COMMAND = CLASS_STRING + "#rescaleCommand"; /** * 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 minimum scale which the TSimStdout2Pane can be scaled to. */ public static final int MIN_SCALE = 2; /** * The maxmimum scale which the TSimStdout2Pane can be scaled to. */ public static final int MAX_SCALE = TImageManager.MAX_SCALE[TImageManager.STDIO_SCALED_IMAGE_GROUP]; /** * The scale used in determining the preferred size of the TSimStdout2Pane. */ public static final int PREFERRED_SCALE = 7; private static final Integer INTEGERS[] = new Integer[MAX_SCALE]; private static final Font FONT_CACHE[] = new Font[MAX_SCALE]; private static final int UNSCALED_MINIMUM_PAGE_WIDTH = 38; private static final int UNSCALED_PREFERRED_PAGE_WIDTH = 38 + 10; private static final int UNSCALED_HEIGHT = 84; private static final int ROW_COUNT = 40; private static final Dimension PREFERRED_SCROLLABLE_VIEWPORT_SIZE = new Dimension( UNSCALED_PREFERRED_PAGE_WIDTH * PREFERRED_SCALE, UNSCALED_HEIGHT * PREFERRED_SCALE ); private static final Color BACKGROUND = new Color(204, 204, 204); private static final Color UNSELECTED_HOLE_COLOR = BACKGROUND; private static final Color UNSELECTED_HOLE_HIGHLIGHT_COLOR = new Color(177, 137, 75); private static final Color UNSELECTED_HOLE_SHADOW_COLOR = new Color(129, 100, 55); private static final Color SELECTION_BORDER_COLOR = new Color(102, 102, 154); private static final Color SELECTED_HOLE_COLOR = Color.white; private static final Color SELECTED_HOLE_HIGHLIGHT_COLOR = new Color(145, 146, 183); private static final Color SELECTED_HOLE_SHADOW_COLOR = new Color(102, 102, 154); private static final Color SELECTED_TEXT_COLOR = Color.black; private static final Border PLACEHOLDER_BORDER = new EtchedBorder(EtchedBorder.LOWERED); /** * Initialize the INTEGERS array. */ static { for (int ctr = 0; ctr < MAX_SCALE; ctr++) INTEGERS[ctr] = new Integer(ctr); } private int cardCount; private int unscaledWidth; private int scale; private int offsetX; private int offsetY; private JLabel selectedTextRenderer; private Image disabledCardImage; private Image unselectedCardImage; private Image selectedCardImage; private Runner runner; private boolean showPlaceholders; private JLabel placeholderLabel; private int newScale; private int selectedIndex; private int dragOffset; TWordBuffer stdout; TWordBuffer oldStdout; private Listener listener; private TVirtualMachine virtualMachine; /** * Creates a new TSimStdout2Pane. */ public TSimStdout2Pane(TVirtualMachine virtualMachine) { super(); setForeground(Color.black); if (virtualMachine == null) throw new NullPointerException(); this.virtualMachine = virtualMachine; stdout = new TWordBuffer(); oldStdout = new TWordBuffer(); runner = new Runner(); listener = new Listener(); virtualMachine.addChangeListener(listener); selectedTextRenderer = new JLabel( "", SwingConstants.LEFT ); selectedTextRenderer.setOpaque(false); selectedTextRenderer.setVerticalTextPosition(SwingConstants.CENTER); selectedTextRenderer.setForeground(SELECTED_TEXT_COLOR); placeholderLabel = new JLabel( "Visual X-TOY is curently resizing the graphics needed for sim mode...", SwingConstants.CENTER ); placeholderLabel.setOpaque(false); placeholderLabel.setVerticalTextPosition(SwingConstants.CENTER); selectedIndex = -1; scale = -1; doCommand( RESCALE_COMMAND, INTEGERS[TImageManager.ORIGINAL_IMAGE_SCALES[TImageManager.STDIO_SCALED_IMAGE_GROUP]] ); setMinimumSize( new Dimension( 1 * MIN_SCALE, UNSCALED_HEIGHT * MIN_SCALE ) ); enableEvents(AWTEvent.COMPONENT_EVENT_MASK | AWTEvent.HIERARCHY_BOUNDS_EVENT_MASK | AWTEvent.MOUSE_EVENT_MASK | AWTEvent.MOUSE_MOTION_EVENT_MASK); } /** * Sets the virtualMachine attached to this TSimStdout2Pane. * * @param virtualMachine The virtualMachine to be attached to this TSimStdout2Pane. */ 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, Boolean.FALSE); } /** * Returns the virtualMachine attached to this TSimStdout2Pane. * * @return The virtualMachine attached to this TSimStdout2Pane. */ 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 == RESCALE_COMMAND) { int scale = ((Integer)extraInfo).intValue(); if (scale < MIN_SCALE || scale > MAX_SCALE) throw new IllegalArgumentException(); if (scale == this.scale) { repaint(); return false; } Dimension preferredSize = getPreferredSize(); preferredSize.width = 2 * offsetX + unscaledWidth * scale; setPreferredSize(preferredSize); this.scale = scale; if (FONT_CACHE[scale] == null) FONT_CACHE[scale] = new Font("Monospaced", Font.BOLD, (int)(0.9 * 2 * scale)); selectedTextRenderer.setFont(FONT_CACHE[scale]); selectedTextRenderer.setBounds(0, 0, 11 * scale, 2 * scale); disabledCardImage = TImageManager.getImage( TImageManager.STDIO_SCALED_IMAGE_GROUP, scale, TImageManager.STDIO_SCALED_PUNCHCARD_DISABLED ); unselectedCardImage = TImageManager.getImage( TImageManager.STDIO_SCALED_IMAGE_GROUP, scale, TImageManager.STDIO_SCALED_PUNCHCARD_UNSELECTED ); selectedCardImage = TImageManager.getImage( TImageManager.STDIO_SCALED_IMAGE_GROUP, scale, TImageManager.STDIO_SCALED_PUNCHCARD_SELECTED ); revalidate(); repaint(); return true; } //////////////////////////////////////////////////////////////////////////////////////////// if (command == UPDATE_COMMAND) { { // Swap the buffers TWordBuffer tempBuffer = oldStdout; oldStdout = stdout; stdout = tempBuffer; } virtualMachine.getStdout(stdout); int cardCount = Math.max(1, (stdout.getSize() + ROW_COUNT - 1) / ROW_COUNT); if (this.cardCount != cardCount) { this.cardCount = cardCount; unscaledWidth = cardCount * 48 - 10; Dimension preferredSize = getPreferredSize(); preferredSize.width = 2 * offsetX + unscaledWidth * scale; setPreferredSize(preferredSize); revalidate(); } if (!stdout.equals(oldStdout)) { if (extraInfo != null && ((Boolean)extraInfo).booleanValue()) { scrollToFrontier(); } else { repaint(); } } return true; } throw new IllegalArgumentException(); } /** * Override paintComponent() to make this TSimStdout2Pane actually paint the memory information. */ public void paintComponent(Graphics g) { Rectangle clip = g.getClipBounds(); int scale = this.scale; // Clear the background g.setColor(BACKGROUND); g.fillRect(clip.x, clip.y, clip.width, clip.height); int offsetX = this.offsetX; int offsetY = this.offsetY; int cardStart = Math.max(0, ((clip.x - offsetX) / scale) / 48); int cardEnd = Math.min(cardCount - 1, ((clip.x + clip.width - offsetX) / scale) / 48); // If the graphics arn't ready, show placeholders instead of cards if (showPlaceholders) { scale = this.newScale; placeholderLabel.setBounds(0, 0, 48 * scale, getHeight()); for (int cardCtr = cardStart; cardCtr <= cardEnd; cardCtr++) { int cardX = offsetX + (cardCtr * 48) * scale; int cardY = offsetY; PLACEHOLDER_BORDER.paintBorder(this, g, cardX, cardY, 38 * scale, 84 * scale); g.translate(cardX - 5 * scale, 0); placeholderLabel.paint(g); g.translate(-(cardX - 5 * scale), 0); } return; } int holeWidth = 2 * scale; int holeHeight = (scale + 1) / 2; TWordBuffer stdout = this.stdout; int stdoutSize = stdout.getSize(); int selectedIndex = this.selectedIndex; if (selectedIndex == -1) selectedIndex = stdoutSize - 1; Image disabledCardImage = this.disabledCardImage; Image unselectedCardImage = this.unselectedCardImage; Image selectedCardImage = this.selectedCardImage; for (int cardCtr = cardStart; cardCtr <= cardEnd; cardCtr++) { int cardX = offsetX + (cardCtr * 48) * scale; int cardY = offsetY; // Draw the card g.drawImage(unselectedCardImage, cardX, cardY, null); // Draw the disabled section, if applicable if ((cardCtr + 1) * ROW_COUNT > stdoutSize) { int disabledStartRow = Math.max(0, stdoutSize - cardCtr * ROW_COUNT); int disabledStartY; if (disabledStartRow == 0) disabledStartY = 0; else disabledStartY = (9 + 8 * disabledStartRow) * scale / 4; g.drawImage( disabledCardImage, cardX, cardY + disabledStartY, cardX + 38 * scale, cardY + 84 * scale, 0, disabledStartY, 38 * scale, 84 * scale, null ); } // Draw the selected section, if applicable if (selectedIndex >= 0 && selectedIndex / ROW_COUNT == cardCtr) { int selectionRow = selectedIndex % ROW_COUNT; int selectionY = (9 + 8 * selectionRow) * scale / 4; g.drawImage( selectedCardImage, cardX, cardY + selectionY, cardX + 38 * scale, cardY + selectionY + 2 * scale, 0, selectionY, 38 * scale, selectionY + 2 * scale, null ); g.setColor(SELECTION_BORDER_COLOR); g.drawLine( cardX, cardY + selectionY, cardX + 38 * scale - 1, cardY + selectionY ); g.drawLine( cardX + 38 * scale - 1, cardY + selectionY, cardX + 38 * scale - 1, cardY + selectionY + 2 * scale - 1 ); g.drawLine( cardX, cardY + selectionY + 2 * scale - 1, cardX + 38 * scale - 1, cardY + selectionY + 2 * scale - 1 ); g.drawLine( cardX, cardY + selectionY, cardX, cardY + selectionY + 2 * scale - 1 ); g.translate(cardX + 27 * scale, (cardY + selectionY)); selectedTextRenderer.setText(stdout.getWord(selectedIndex).toHexString(false)); selectedTextRenderer.paint(g); g.translate(-(cardX + 27 * scale), -(cardY + selectionY)); } // Draw the holes for (int rowCtr = 0; rowCtr < ROW_COUNT; rowCtr++) { int index = cardCtr * ROW_COUNT + rowCtr; if (index < stdoutSize) { short value = stdout.getWord(index).getValue(); if (value == 0) continue; Color holeColor; Color holeHighlightColor; Color holeShadowColor; if (index == selectedIndex) { holeColor = SELECTED_HOLE_COLOR; holeHighlightColor = SELECTED_HOLE_HIGHLIGHT_COLOR; holeShadowColor = SELECTED_HOLE_SHADOW_COLOR; } else { holeColor = UNSELECTED_HOLE_COLOR; holeHighlightColor = UNSELECTED_HOLE_HIGHLIGHT_COLOR; holeShadowColor = UNSELECTED_HOLE_SHADOW_COLOR; } int holeY0 = cardY + (5 + 4 * rowCtr) * scale / 2; int holeY1 = cardY + (5 + 4 * rowCtr + 2) * scale / 2; for (int ctr = 0; ctr < 8; ctr++) { int holeX = cardX + (5 + 6 * ctr) * scale / 2; if (((value >> (15 - ctr)) & 1) != 0) { g.setColor(holeColor); g.fillRect(holeX, holeY0, holeWidth, holeHeight); g.setColor(holeHighlightColor); g.drawLine( holeX, holeY0 + holeHeight - 1, holeX + holeWidth - 1, holeY0 + holeHeight - 1 ); g.drawLine( holeX, holeY0, holeX, holeY0 + holeHeight - 1 ); g.setColor(holeShadowColor); g.drawLine( holeX, holeY0, holeX + holeWidth - 1, holeY0 ); g.drawLine( holeX + holeWidth - 1, holeY0, holeX + holeWidth - 1, holeY0 + holeHeight - 1 ); } if (((value >> (7 - ctr)) & 1) != 0) { g.setColor(holeColor); g.fillRect(holeX, holeY1, holeWidth, holeHeight); g.setColor(holeHighlightColor); g.drawLine( holeX, holeY1 + holeHeight - 1, holeX + holeWidth - 1, holeY1 + holeHeight - 1 ); g.drawLine( holeX, holeY1, holeX, holeY1 + holeHeight - 1 ); g.setColor(holeShadowColor); g.drawLine( holeX, holeY1, holeX + holeWidth - 1, holeY1 ); g.drawLine( holeX + holeWidth - 1, holeY1, holeX + holeWidth - 1, holeY1 + holeHeight - 1 ); } } } } } } /** * Intercept HierarchyEvents to do resize and move the detailPanel when this component is * resized. */ protected void processHierarchyBoundsEvent(HierarchyEvent e) { if (e.getID() == HierarchyEvent.ANCESTOR_RESIZED && e.getChanged() == getParent() && e.getChanged() instanceof JViewport) { deriveScale(); } } /** * Intercept ComponentEvents to do resize and move the memPanel when this component is * resized. */ protected void processComponentEvent(ComponentEvent e) { if (e.getID() == ComponentEvent.COMPONENT_RESIZED) deriveScale(); super.processComponentEvent(e); } /** * Process MouseEvents to mark the beginning of a mouse dragging session and to catch the mouse * leaving. */ protected void processMouseEvent(MouseEvent e) { switch (e.getID()) { case MouseEvent.MOUSE_PRESSED: if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) { Container parent = getParent(); if (parent instanceof JViewport) dragOffset = e.getX() + getX() + ((JViewport)parent).getViewPosition().x; } break; case MouseEvent.MOUSE_EXITED: if (selectedIndex != -1 && (e.getModifiers() & MouseEvent.BUTTON1_MASK) == 0) { selectedIndex = -1; repaint(); } break; } super.processMouseEvent(e); } /** * Process MouseEvents to scroll the pane when the mouse is dragged and get the selectedIndex * of the component. */ protected void processMouseMotionEvent(MouseEvent e) { switch (e.getID()) { case MouseEvent.MOUSE_DRAGGED: if ((e.getModifiers() & MouseEvent.BUTTON1_MASK) != 0) { Container parent = getParent(); if (parent instanceof JViewport) { JViewport viewport = ((JViewport)parent); Rectangle rectangle = viewport.getViewRect(); int newPosition = dragOffset - (e.getX() + getX()); int paneWidth = getPreferredSize().width; if (newPosition + rectangle.width > paneWidth) newPosition = paneWidth - rectangle.width; if (newPosition < 0) newPosition = 0; viewport.setViewPosition(new Point(newPosition, rectangle.y)); } } break; case MouseEvent.MOUSE_MOVED: { int tempInt = (e.getX() - offsetX) / scale; int selectedIndex = -1; if (tempInt >= 0 && tempInt % 48 < 38) { int card = tempInt / 48; int row = (((e.getY() - offsetY) * 4) / scale - 9) / 8; if (row >= 0 && row < ROW_COUNT) { selectedIndex = card * ROW_COUNT + row; if (selectedIndex >= stdout.getSize()) selectedIndex = -1; } } if (selectedIndex != this.selectedIndex) { this.selectedIndex = selectedIndex; repaint(); } } break; } super.processMouseEvent(e); } /** * Determines the optimal scale, and then executes the RESCALE_COMMAND. */ private void deriveScale() { int height = getHeight(); int scale = height / UNSCALED_HEIGHT; if (getParent() instanceof JViewport) { int width = ((JViewport)getParent()).getWidth(); scale = Math.min(scale, width / UNSCALED_MINIMUM_PAGE_WIDTH); } if (scale < MIN_SCALE) scale = MIN_SCALE; if (scale > MAX_SCALE) scale = MAX_SCALE; offsetX = 5 * scale; offsetY = height / 2 - UNSCALED_HEIGHT * scale / 2; synchronized (this) { this.newScale = scale; if (!runner.isRunning) { if (!TImageManager.isPrepared(TImageManager.STDIO_SCALED_IMAGE_GROUP, scale)) { showPlaceholders = true; setCursor(Cursor.getPredefinedCursor(Cursor.WAIT_CURSOR)); Dimension preferredSize = getPreferredSize(); preferredSize.width = 2 * offsetX + unscaledWidth * scale; setPreferredSize(preferredSize); PSwingUtilities.revalidateImmediately(this); PSwingUtilities.paintWindowImmediately(this); runner.start(); } else { doCommand(RESCALE_COMMAND, INTEGERS[scale]); } } else { Dimension preferredSize = getPreferredSize(); preferredSize.width = 2 * offsetX + unscaledWidth * scale; setPreferredSize(preferredSize); PSwingUtilities.revalidateImmediately(this); PSwingUtilities.paintWindowImmediately(this); } } } /** * Ensures that the cell representing the word at the frontier of the stdout list is visible. */ public void scrollToFrontier() { int selectedCard = (stdout.getSize() - 1) / ROW_COUNT; scrollRectToVisible( new Rectangle( (selectedCard * 48) * scale + offsetX, 0, 38 * scale, getHeight() ) ); repaint(); } /** * Implement scrollable to make the viewport's preferred size that of the page size at * the PREFERRED_SCALE. */ public Dimension getPreferredScrollableViewportSize() { return PREFERRED_SCROLLABLE_VIEWPORT_SIZE; } /** * Implement scrollable to make the unit increment a tenth of a page. */ public int getScrollableUnitIncrement(Rectangle visibleRect, int orientation, int direction) { switch(orientation) { case SwingConstants.VERTICAL: return visibleRect.height / 10; case SwingConstants.HORIZONTAL: return visibleRect.width / 10; default: throw new IllegalArgumentException("Invalid orientation: " + orientation); } } /** * Implement scrollable to make the block increment a full page. */ public int getScrollableBlockIncrement(Rectangle visibleRect, int orientation, int direction) { switch(orientation) { case SwingConstants.VERTICAL: return visibleRect.height; case SwingConstants.HORIZONTAL: return visibleRect.width; default: throw new IllegalArgumentException("Invalid orientation: " + orientation); } } /** * Implement scrollable to make the InternalTextArea's size the greater of its own size * and its viewport's size. */ public boolean getScrollableTracksViewportWidth() { if (getParent() instanceof JViewport) { return (((JViewport)getParent()).getWidth() > getPreferredSize().width); } return false; } /** * Implement scrollable to make the InternalTextArea's height always equal to that of it's * viewport's. */ public boolean getScrollableTracksViewportHeight() { return true; } /** * The Listener of a TBaseConverterPane pays attention to the changes in the PTextFields, and * fires off changes to the other PTextFields. * * @author btsang * @version 7.1 */ protected class Listener implements ChangeListener { /** * Implement ChangeListener to update the list whenever the virtual machine's state changes. */ public void stateChanged(ChangeEvent e) { doCommand(UPDATE_COMMAND, Boolean.TRUE); } } /** * Runner is a simple implementation of Runnable for the Thread started when the component's * size changes. * * @author btsang * @version 7.1 */ protected class Runner implements Runnable { protected boolean isRunning; private Thread thread; protected Runner() { isRunning = false; } /** * Causes the runner to start. This should only be called by a Thread which has * synchronized the TSimStdout2Pane and has checked that the Runner is not already running. */ public void start() { isRunning = true; thread = new Thread(this); thread.start(); } /** * Implement Runnable to run the TSimStdout2Pane. */ public void run() { int scale; while (true) { synchronized (TSimStdout2Pane.this) { scale = newScale; if (TImageManager.isPrepared(TImageManager.STDIO_SCALED_IMAGE_GROUP, scale)) { doCommand(RESCALE_COMMAND, INTEGERS[scale]); showPlaceholders = false; setCursor(Cursor.getPredefinedCursor(Cursor.DEFAULT_CURSOR)); repaint(); isRunning = false; thread = null; return; } } TImageManager.prepare(TImageManager.STDIO_SCALED_IMAGE_GROUP, scale); } } } }