package edu.cmu.hcii.whyline.ui.io; import java.awt.*; import java.awt.event.KeyEvent; import java.awt.event.MouseEvent; import java.awt.geom.Rectangle2D; import java.awt.image.BufferedImage; import java.util.TimerTask; import javax.swing.ImageIcon; import edu.cmu.hcii.whyline.bytecode.*; import edu.cmu.hcii.whyline.io.*; import edu.cmu.hcii.whyline.qa.*; import edu.cmu.hcii.whyline.source.*; import edu.cmu.hcii.whyline.trace.*; import edu.cmu.hcii.whyline.ui.*; import edu.cmu.hcii.whyline.ui.qa.EventView; import edu.cmu.hcii.whyline.ui.qa.VisualizationUI; import edu.cmu.hcii.whyline.ui.views.*; /** * @author Andrew J. Ko * */ public final class TimeUI extends DynamicComponent implements UserQuestionListener, UserTimeListener, UserFocusListener { private final WhylineUI whylineUI; private BufferedImage ioEventMarkers; private double progress = 0.0; private static final int BUTTON_PADDING = 6; private final IOButton repaint, move, drag, press, release, print, read, scroll, keyup, keydown; private IOButton selection = null; public TimeUI(WhylineUI whylineUI) { super(whylineUI, Sizing.FIT, Sizing.FIT); this.whylineUI = whylineUI; setView(controller); move = new IOButton("mouse move events", UI.MOUSE_MOVE_ICON, (UI.ICON_SIZE + BUTTON_PADDING) * 0); drag = new IOButton("mouse drag events", UI.MOUSE_DRAG_ICON, (UI.ICON_SIZE + BUTTON_PADDING) * 1); press = new IOButton("mouse press events", UI.MOUSE_DOWN_ICON, (UI.ICON_SIZE + BUTTON_PADDING) * 2); release = new IOButton("mouse release events", UI.MOUSE_UP_ICON, (UI.ICON_SIZE + BUTTON_PADDING) * 3); scroll = new IOButton("scroll wheel events", UI.MOUSE_WHEEL_ICON, (UI.ICON_SIZE + BUTTON_PADDING) * 4); keyup = new IOButton("key up events", UI.KEY_UP_ICON, (UI.ICON_SIZE + BUTTON_PADDING) * 5); keydown = new IOButton("key down events", UI.KEY_DOWN_ICON, (UI.ICON_SIZE + BUTTON_PADDING) * 6); repaint = new IOButton("repaint events", UI.REPAINT_ICON, (UI.ICON_SIZE + BUTTON_PADDING) * 7); print = new IOButton("print to console events", UI.CONSOLE_OUT_ICON, (UI.ICON_SIZE + BUTTON_PADDING) * 8); read = new IOButton("read from console events", UI.CONSOLE_IN_ICON, (UI.ICON_SIZE + BUTTON_PADDING) * 9); controller.addChild(move); controller.addChild(drag); controller.addChild(press); controller.addChild(release); controller.addChild(scroll); controller.addChild(keyup); controller.addChild(keydown); controller.addChild(repaint); controller.addChild(print); controller.addChild(read); updateSize(); setToolTipText(whylineUI.isWhyline() ? Tooltips.TIME_CURSOR : "<html>Represents the program execution, from start to finish.<br>Drag to see the state of program output at different times"); } public void setProgress(double percent) { progress = percent; repaint(); } private double convertEventIndexToPosition(long eventIndex) { int numberOfEvents = whylineUI.getTrace().getNumberOfEvents(); return (((double)eventIndex / numberOfEvents) * controller.getLocalWidth()); } private void redrawIOEventMarkers() { if(!whylineUI.getTrace().isDoneLoading()) return; ioEventMarkers = new BufferedImage((int)Math.max(1, controller.getLocalWidth()), (int)Math.max(1, controller.getLocalHeight()), BufferedImage.TYPE_INT_ARGB); Graphics2D g = (Graphics2D)ioEventMarkers.getGraphics(); int verticalCenter = (int)controller.getLocalHeight() / 2; int maxTime = whylineUI.getTrace().getNumberOfEvents(); g.setColor(UI.getControlTextColor()); for(IOEvent event : whylineUI.getTrace().getIOHistory()) { if(event.segmentsOutput() && ioEventIsOfDesiredType(event)) { int left = (int)convertEventIndexToPosition(event.getEventID()); g.fillRect(left, verticalCenter, 2,2); } } } private boolean breakpointDebuggerIsRunning() { return whylineUI.getMode() == WhylineUI.Mode.BREAKPOINT && whylineUI.getBreakpointDebugger().isRunning(); } /** * If an input is selected (I), scope to next output (R or T) * If an output is selected (I), scope to next output (R or T) */ private void setInputOutputTimes(IOEvent inputEvent) { IOEvent outputEvent = determineSelectableIOEventAfter(inputEvent); whylineUI.setInputTime(inputEvent == null ? 0 : inputEvent.getEventID()); // Always points to the last event in the trace. whylineUI.setOutputTime(whylineUI.getTrace().getNumberOfEvents() - 1); } /** * Inputs are selectable (I) * Outputs that are followed by outputs are selectable * Outputs that are followed by input are NOT selectable */ private IOEvent determineSelectableIOEventAfter(IOEvent event) { if(event == whylineUI.getTrace().getIOHistory().getLastEvent()) return event; for(IOEvent io : whylineUI.getTrace().getIOHistory().getIteratorForEventsAfter(event)) if(io.segmentsOutput() && ioEventIsOfDesiredType(io)) return io; return event; } private IOEvent determineSelectableIOEventAtOrBefore(IOEvent event) { if(event == null) return event; if(event.segmentsOutput() && ioEventIsOfDesiredType(event)) return event; for(IOEvent io : whylineUI.getTrace().getIOHistory().getIteratorForEventsBefore(event)) if(io.segmentsOutput() && ioEventIsOfDesiredType(io)) return io; return null; } public void questionChanged(Question<?> question) { updateSize(); } public boolean isMinimized() { return !whylineUI.isWhyline() || whylineUI.getQuestionVisible() != null; } private void updateSize() { setPreferredSize(new Dimension(0, isMinimized() ? UI.TIME_UI_HEIGHT / 3: UI.TIME_UI_HEIGHT)); revalidate(); } public void inputTimeChanged(int time) { repaint(); } public void outputTimeChanged(int time) { repaint(); } public int getHorizontalScrollIncrement() { return 0; } public int getVerticalScrollIncrement() { return 0; } public boolean ioEventIsOfDesiredType(IOEvent io) { if(selection == null) return true; if(io instanceof MouseStateInputEvent) { MouseStateInputEvent mouseEvent = (MouseStateInputEvent)io; int id = mouseEvent.getType(); switch(id) { case MouseEvent.MOUSE_PRESSED : return selection == press; case MouseEvent.MOUSE_DRAGGED : return selection == drag; case MouseEvent.MOUSE_CLICKED : case MouseEvent.MOUSE_RELEASED : return selection == release; case MouseEvent.MOUSE_WHEEL : return selection == scroll; case MouseEvent.MOUSE_MOVED : return selection == move; default : return false; } } else if(io instanceof KeyStateInputEvent) { KeyStateInputEvent keyEvent = (KeyStateInputEvent)io; int type = keyEvent.getType(); switch(type) { case KeyEvent.KEY_PRESSED : return selection == keydown; case KeyEvent.KEY_RELEASED : return selection == keyup; case KeyEvent.KEY_TYPED : return selection == keyup; default: return false; } } else if(selection == repaint) return io instanceof GetGraphicsOutputEvent; else if(selection == print) return io instanceof TextualOutputEvent; else return false; } public void selectButtonBasedOnTrace() { Trace trace = whylineUI.getTrace(); // if(trace.getRenderHistory().getNumberOfEvents() > 0) { // selection = press; // } // else { // selection = print; // } selection = null; validate(); repaint(); } private final View controller = new View() { public boolean handleMouseDown(int localX, int localY, int mouseButton) { getContainer().focusMouseOn(this); return handleMouseDrag(localX, localY, mouseButton); } public boolean handleMouseDrag(int localX, int localY, int mouseButton) { if(breakpointDebuggerIsRunning()) return false; if(whylineUI.getMode() == WhylineUI.Mode.BREAKPOINT) { int time = (int)((localX / getLocalWidth()) * whylineUI.getTrace().getNumberOfEvents()); whylineUI.setInputTime(time); whylineUI.getBreakpointDebugger().setPauseMode(true); } else { // Convert the position into a trace time, then find the IO event nearest it. int time = (int)((localX / getLocalWidth()) * whylineUI.getTrace().getNumberOfEvents()); IOEvent io = whylineUI.getTrace().getIOHistory().getMostRecentBeforeTime(time); if(whylineUI.isQuestionVisible()) { Answer answer = whylineUI.getVisualizationUIVisible().getAnswer(); if(io != null && answer != null) { Explanation explanation = answer.getExplanationFor(io.getEventID()); answer.broadcastChanges(); if(explanation != null) whylineUI.selectExplanation(explanation, false, UI.TIME_UI); } } else setInputOutputTimes(determineSelectableIOEventAtOrBefore(io)); } return true; } public boolean handleMouseUp(int localX, int localY, int mouseButton) { getContainer().releaseMouseFocus(); return true; } public boolean handleKeyPressed(KeyEvent e) { if(breakpointDebuggerIsRunning()) return false; Trace trace = whylineUI.getTrace(); // Get the io event before the current one. if(e.getKeyCode() == KeyEvent.VK_LEFT) { // Get event before current IOEvent ioEvent = whylineUI.getTrace().getIOHistory().getMostRecentBeforeTime(whylineUI.getInputEventID() - 1); setInputOutputTimes(determineSelectableIOEventAtOrBefore(ioEvent)); return true; } // Get the io event after the current one. else if(e.getKeyCode() == KeyEvent.VK_RIGHT) { setInputOutputTimes(determineSelectableIOEventAfter(whylineUI.getEventAtInputTime())); return true; } else return false; } public void paintBelowChildren(Graphics2D g) { g = (Graphics2D)g.create(); int left = (int) convertEventIndexToPosition(whylineUI.getInputEventID()); int right = (int) convertEventIndexToPosition(whylineUI.getOutputEventID()); int scopeLeft = left; int scopeRight = right; // If there's a question showing, show the scope used at the time of the question. if(whylineUI.isQuestionVisible()) { scopeLeft = (int) convertEventIndexToPosition(whylineUI.getQuestionVisible().getInputEventID()); scopeRight = (int) convertEventIndexToPosition(whylineUI.getQuestionVisible().getOutputEventID()); } int verticalCenter = (int)(getLocalHeight() / 2); int boxWidth = (int)(getLocalHeight() / 4); Color lockedColor = Color.lightGray; Color draggingColor = Color.white; Color setColor = UI.getHighlightColor(); Paint oldPaint = g.getPaint(); Composite oldComposite = g.getComposite(); if(whylineUI.getQuestionOver() != null) { Question<?> q = whylineUI.getQuestionOver(); int x, w; if(q.isPhrasedNegatively()) { x = scopeLeft; w = scopeRight - scopeLeft; } else { x = 0; w = scopeLeft; } g.setColor(UI.getHighlightColor()); g.fillRect(x, 0, w, (int)getLocalHeight()); g.setColor(UI.getHighlightTextColor()); } // Draw the time controller g.setColor(breakpointDebuggerIsRunning() ? UI.getRunningColor() : UI.getControlTextColor()); g.fillRoundRect(scopeLeft, 0, 4, (int)getLocalHeight(), 5, 5); g.setComposite(oldComposite); g.setPaint(oldPaint); // If there is a question showing, show the eventID of the current selection. if(whylineUI.isQuestionVisible()) { g.setColor(UI.getControlTextColor()); g.setStroke(UI.SELECTED_STROKE); g.drawLine(left, 0, left, (int)getLocalHeight()); VisualizationUI viz = whylineUI.getVisualizationUIVisible(); if(viz != null) { if(viz.getSelection() instanceof EventView) { int eventID = ((EventView)viz.getSelection()).getEventID(); left = (int) convertEventIndexToPosition(eventID); } } g.setColor(UI.getHighlightColor()); g.setStroke(UI.SELECTED_STROKE); g.drawLine(left, 0, left, (int)getLocalHeight()); } // If there's no question showing, draw the description of the I/O event and the filter message. if(!isMinimized()) { g.setColor(UI.getControlTextColor()); // Draw the event description int inputEventID = whylineUI.getInputEventID(); IOEvent ioEvent = whylineUI.getEventAtInputTime(); String eventDescription = inputEventID == 0 ? "program started..." : (ioEvent == null ? "" : ioEvent.getHTMLDescription()); boolean negative = false; if(whylineUI.getQuestionOver() != null) { negative = whylineUI.getQuestionOver().isPhrasedNegatively(); eventDescription = (negative ? "after this " : "before this ") + eventDescription + "..."; } g.setFont(UI.getSmallFont()); Rectangle2D bounds = g.getFontMetrics().getStringBounds(eventDescription, g); int labelX = (int) (negative ? scopeLeft - bounds.getWidth() - 10 : scopeLeft + 10); labelX = Math.max(0, labelX); labelX = (int) Math.min(labelX, (int)getLocalWidth() - bounds.getWidth() - 10); g.drawString(eventDescription, labelX, (int)getLocalBottom() - 10); // Draw the filter message int filterMessageX = (int) (getLastChild().getLocalRight() + UI.getPanelPadding()); int filterMessageY = (int) (getLastChild().getLocalBottom()); g.setColor(UI.getControlTextColor()); g.setFont(UI.getSmallFont()); g.drawString(selection == null ? "showing all i/o events" : "only showing " + selection.description, filterMessageX, filterMessageY); } } public void paintAboveChildren(Graphics2D g) { // Paint the i/o markers, but only in whyline mode if(whylineUI.isWhyline()) { if(ioEventMarkers == null) redrawIOEventMarkers(); g.drawImage(ioEventMarkers, 0, 0, null); } } public void handleContainerResize() { redrawIOEventMarkers(); } }; public void showEvent(int eventID) { if(!whylineUI.isQuestionVisible()) return; // We only need to update this if the NEW most RECENT IO event is a DIFFERENT render event than the last one drawn. IOEvent e = whylineUI.getTrace().getIOHistory().getMostRecentBeforeTime(eventID); if(e instanceof RenderEvent) whylineUI.setInputTime(e == null ? 0 : e.getEventID()); repaint(); } public void showExplanation(Explanation subject) { showEvent(subject.getEventID()); } public void showFile(FileInterface subject) {} public void showInstruction(Instruction subject) {} public void showInstructions(Iterable<? extends Instruction> subject) {} public void showMethod(MethodInfo subject) {} public void showClass(Classfile subject) {} public void showUnexecutedInstruction(UnexecutedInstruction subject) {} private java.util.Timer flasher; private double flashingAmount = 0; private int flashingFrequency = 75; private double getPercentFlash() { return Math.abs(Math.sin(Math.PI * flashingAmount / 180.0 )); } public void startFlashingMessage() { if(flasher != null) return; flasher = new java.util.Timer("Flasher", true); flasher.schedule(new TimerTask() { public void run() { flashingAmount += 180 / (1000 / flashingFrequency); if(flashingAmount > 180) flashingAmount = 0; repaint(); } }, 0, flashingFrequency); } public void stopFlashingMessage() { if(flasher != null) flasher.cancel(); flasher = null; } private class IOButton extends View { private final ImageIcon icon; private final String description; public IOButton(String description, ImageIcon icon, int left) { this.description = description; this.icon = icon; setLocalLeft(left + BUTTON_PADDING, false); setLocalTop(BUTTON_PADDING, false); setLocalWidth(UI.ICON_SIZE, false); setLocalHeight(UI.ICON_SIZE, false); } public boolean handleMouseDown(int x, int y, int button) { if(isDisabled()) return false; if(selection == this) selection = null; else selection = this; redrawIOEventMarkers(); repaint(); return true; } private boolean isDisabled() { return whylineUI.isQuestionVisible() || !whylineUI.isWhyline(); } public void paintAboveChildren(Graphics2D g) { if(isDisabled()) return; Graphics2D scaled = (Graphics2D)g.create(); // If something is selected and its not this, cross this out. if(selection != this) { scaled.setComposite(AlphaComposite.getInstance(AlphaComposite.SRC_OVER, UI.DESELECTED_ICON_TRANSPARENCY)); } icon.paintIcon(TimeUI.this, scaled, (int)getVisibleLocalLeft(), (int)getVisibleLocalTop()); } } }