content: 
  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());
  
  		}
  		
  	}
  
  }