/*
* Copyright 2008 Google Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License"); you may not
* use this file except in compliance with the License. You may obtain a copy of
* the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
* WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
* License for the specific language governing permissions and limitations under
* the License.
*/
package com.google.speedtracer.client;
import com.google.gwt.coreext.client.JSOArray;
import com.google.gwt.dom.client.DivElement;
import com.google.gwt.dom.client.Element;
import com.google.gwt.dom.client.Style.Unit;
import com.google.gwt.graphics.client.Color;
import com.google.gwt.resources.client.CssResource;
import com.google.gwt.topspin.ui.client.ClickEvent;
import com.google.gwt.topspin.ui.client.ClickListener;
import com.google.gwt.topspin.ui.client.Container;
import com.google.gwt.topspin.ui.client.DefaultContainerImpl;
import com.google.gwt.topspin.ui.client.Div;
import com.google.gwt.topspin.ui.client.InsertingContainerImpl;
import com.google.gwt.topspin.ui.client.KeyDownEvent;
import com.google.gwt.topspin.ui.client.KeyUpEvent;
import com.google.gwt.topspin.ui.client.MouseDownEvent;
import com.google.gwt.topspin.ui.client.MouseDownListener;
import com.google.gwt.topspin.ui.client.ResizeEvent;
import com.google.gwt.topspin.ui.client.ResizeListener;
import com.google.gwt.topspin.ui.client.Window;
import com.google.gwt.user.client.Timer;
import com.google.speedtracer.client.model.ApplicationState;
import com.google.speedtracer.client.model.ButtonDescription;
import com.google.speedtracer.client.model.DomContentLoadedEvent;
import com.google.speedtracer.client.model.HintRecord;
import com.google.speedtracer.client.model.HintletInterface;
import com.google.speedtracer.client.model.TabChangeDispatcher;
import com.google.speedtracer.client.model.TabChangeEvent;
import com.google.speedtracer.client.model.UiEventDispatcher;
import com.google.speedtracer.client.model.Visualization;
import com.google.speedtracer.client.model.WindowLoadEvent;
import com.google.speedtracer.client.timeline.Constants;
import com.google.speedtracer.client.timeline.GraphModel;
import com.google.speedtracer.client.timeline.TimeLineModel;
import com.google.speedtracer.client.timeline.fx.Zoom;
import com.google.speedtracer.client.timeline.fx.Zoom.CallBack;
import com.google.speedtracer.client.util.Url;
import com.google.speedtracer.client.util.dom.DocumentExt;
import com.google.speedtracer.client.util.dom.EventListenerOwner;
import com.google.speedtracer.client.view.Controller;
import com.google.speedtracer.client.view.DetailViews;
import com.google.speedtracer.client.view.MainTimeLine;
import com.google.speedtracer.client.view.OverViewTimeLine;
import com.google.speedtracer.client.view.TimeScale;
import com.google.speedtracer.client.view.TimelineMarks;
import com.google.speedtracer.client.view.OverViewTimeLine.OverViewTimeLineModel;
import com.google.speedtracer.client.visualizations.model.NetworkVisualization;
import com.google.speedtracer.client.visualizations.model.NetworkVisualizationModel;
import com.google.speedtracer.client.visualizations.model.SluggishnessModel;
import com.google.speedtracer.client.visualizations.model.SluggishnessVisualization;
import com.google.speedtracer.client.visualizations.model.VisualizationModel;
import com.google.speedtracer.client.visualizations.view.EventRecordColors;
import com.google.speedtracer.shared.EventRecordType;
import java.util.ArrayList;
import java.util.List;
/**
* Panel that contains the main UI components of the Monitor. All the TimeLine
* and other visualizations get attached here.
*/
public class MonitorVisualizationsPanel extends Div implements
TabChangeDispatcher.Listener, UiEventDispatcher.LoadEventListener {
/**
* CSS.
*/
public interface Css extends CssResource {
int borderWidth();
String buttonBar();
String graphContainer();
String tabList();
String tabListEntry();
String tabListEntrySelected();
String timelineContainer();
int timelineHeight();
int topPadding();
String visualizationPanel();
}
/**
* Externalized interface.
*/
public interface Resources extends TimeScale.Resources,
OverViewTimeLine.Resources, MainTimeLine.Resources,
TimelineMarks.Resources {
@Source("resources/MonitorVisualizationsPanel.css")
MonitorVisualizationsPanel.Css monitorVisualizationsPanelCss();
}
/**
* Single object to handle keyboard events and window resizes.
*/
private class EventListener implements ResizeListener, HotKey.Handler {
public void onKeyDown(KeyDownEvent event) {
int keyCode = event.getKeyCode();
if (keyCode == HotKey.LEFT_ARROW || keyCode == HotKey.RIGHT_ARROW) {
jog(keyCode);
}
}
public void onKeyUp(KeyUpEvent event) {
int keyCode = event.getKeyCode();
if (keyCode == HotKey.LEFT_ARROW || keyCode == HotKey.RIGHT_ARROW) {
detailsViewPanel.updateCurrentView(mainTimeLineModel.getLeftBound(),
mainTimeLineModel.getRightBound());
}
}
/**
* Cache the graph dimensions.
*/
public void onResize(ResizeEvent event) {
mainTimeLine.recomputeGraphDimensions();
}
private void jog(int direction) {
double left = mainTimeLineModel.getLeftBound();
double right = mainTimeLineModel.getRightBound();
double delta = (right - left) / 200;
if (direction == HotKey.RIGHT_ARROW) {
mainTimeLineModel.updateBounds(
mainTimeLineModel.getLeftBound() + delta,
mainTimeLineModel.getRightBound() + delta);
}
if (direction == HotKey.LEFT_ARROW) {
mainTimeLineModel.updateBounds(left - delta, right - delta);
}
fixYAxisLabel();
}
}
/**
* Simple overloading subclass that fascilitates short circuiting the updating
* of the details view.
*/
private class ShortCircuitTimeLineModel extends TimeLineModel {
public ShortCircuitTimeLineModel() {
super(false, false);
}
@Override
public void onDomainChange(double newValue) {
loadedState.setLastDomainValue(newValue);
super.onDomainChange(newValue);
}
@Override
public void onModelDataRefreshTick(double now) {
double left = getLeftBound();
double right = getRightBound();
if (now < right) {
detailsViewPanel.updateCurrentView(left, right);
fixYAxisLabel();
}
super.onModelDataRefreshTick(now);
}
@Override
public void updateBounds(double leftBound, double rightBound) {
// update the scale
scale.updateScaleLabels(mainTimeLine.getCurrentGraphWidth(), leftBound,
rightBound);
super.updateBounds(leftBound, rightBound);
}
}
/**
* The list of Tabs on the left.
*/
private class TabList extends Div {
// Container Element for Visualization specific buttons.
private final Element buttonBar;
private final EventListenerOwner listenerOwner = new EventListenerOwner();
private Element previouslySelected;
public TabList(Container container) {
super(container);
Element elem = getElement();
Css css = resources.monitorVisualizationsPanelCss();
elem.setClassName(css.tabList());
elem.getStyle().setPropertyPx("width", Constants.GRAPH_PIXEL_OFFSET);
buttonBar = elem.getOwnerDocument().createDivElement();
buttonBar.setClassName(css.buttonBar());
elem.appendChild(buttonBar);
// Initialize previouslySelected to the default tablist entry.
previouslySelected = createTabs();
if (previouslySelected != null) {
previouslySelected.setClassName(css.tabListEntry() + " "
+ css.tabListEntrySelected());
selectVisualization(visualizations.get(0));
}
}
/**
* Adds visualization specific
* {@link com.google.gwt.topspin.ui.client.Button}s to the buttonBar.
*/
private void addButtonBarButtons(Visualization<?, ?> viz) {
listenerOwner.removeAllEventListeners();
buttonBar.setInnerHTML("");
Container buttonBarContainer = new DefaultContainerImpl(buttonBar);
JSOArray<ButtonDescription> buttons = viz.getButtons();
for (int i = 0, n = buttons.size(); i < n; i++) {
buttons.get(i).createButton(buttonBarContainer, listenerOwner);
}
}
/**
* Creates the tablist entries.
*
* @retutn returns the TabList entry Element that should be used as the
* default selection.
*/
private Element createTabs() {
Element defaultSelection = null;
for (int i = 0, n = visualizations.size(); i < n; i++) {
final Visualization<?, ?> viz = visualizations.get(i);
String tabTitle = viz.getTitle() + " (" + viz.getSubtitle() + ")";
DocumentExt doc = getElement().getOwnerDocument().cast();
final DivElement entry = doc.createDivWithClassName(resources.monitorVisualizationsPanelCss().tabListEntry());
entry.setInnerText(tabTitle);
// The very first one should be flush with the top scale. So we push it
// down a little. Also, we make the first visualization tablist entry be
// the default selection.
if (0 == i) {
defaultSelection = entry;
entry.getStyle().setMarginTop(
resources.monitorVisualizationsPanelCss().topPadding(), Unit.PX);
}
ClickEvent.addClickListener(entry, entry, new ClickListener() {
public void onClick(ClickEvent event) {
selectTab(entry, viz);
}
});
getElement().appendChild(entry);
}
return defaultSelection;
}
private void selectTab(Element entry, Visualization<?, ?> viz) {
previouslySelected.setClassName(resources.monitorVisualizationsPanelCss().tabListEntry());
previouslySelected = entry;
previouslySelected.setClassName(resources.monitorVisualizationsPanelCss().tabListEntry()
+ " "
+ resources.monitorVisualizationsPanelCss().tabListEntrySelected());
selectVisualization(viz);
}
private void selectVisualization(Visualization<?, ?> viz) {
reOrderVisualizations(viz);
detailsViewPanel.setCurrentView(viz);
refresh();
selectedVisualization = viz;
fixYAxisLabel();
addButtonBarButtons(viz);
}
}
private static final int HINTLET_REFRESH_DELAY_MS = 1000;
private final DetailViews detailsViewPanel;
private ApplicationState loadedState;
private final MainTimeLine mainTimeLine;
private final TimeLineModel mainTimeLineModel;
private final OverViewTimeLine overViewTimeLine;
private final Resources resources;
private final TimeScale scale;
private Visualization<?, ?> selectedVisualization;
private final TimelineMarks timelineMarks;
private final List<Visualization<?, ?>> visualizations = new ArrayList<Visualization<?, ?>>();
public MonitorVisualizationsPanel(Container parentContainer,
Controller controller, ApplicationState initialState,
MonitorVisualizationsPanel.Resources resources) {
super(parentContainer);
this.loadedState = initialState;
this.resources = resources;
// Construct UI.
final Css css = resources.monitorVisualizationsPanelCss();
setStyleName(css.visualizationPanel());
Container container = new DefaultContainerImpl(getElement());
DivElement timeLineContainerElem = getElement().getOwnerDocument().createDivElement();
timeLineContainerElem.setClassName(css.timelineContainer());
getElement().appendChild(timeLineContainerElem);
// Create a little wrapper div to wrap the main and overview timelines.
DivElement graphContainerElem = getElement().getOwnerDocument().createDivElement();
graphContainerElem.setClassName(css.graphContainer());
// The left header + 1px border.
graphContainerElem.getStyle().setPropertyPx("left",
Constants.GRAPH_PIXEL_OFFSET + 1);
timeLineContainerElem.appendChild(graphContainerElem);
Container graphContainer = new DefaultContainerImpl(graphContainerElem);
// Add the scale
this.scale = new TimeScale(graphContainer, resources);
// callback to update the details panel when transition changes.
Zoom.CallBack transitionCallback = new CallBack() {
public void onAnimationComplete() {
fixYAxisLabel();
double left = mainTimeLineModel.getLeftBound();
double right = mainTimeLineModel.getRightBound();
detailsViewPanel.updateCurrentView(left, right);
timelineMarks.drawMarksInBounds(left, right);
}
};
// Add the MainTimeLine.
this.mainTimeLineModel = new ShortCircuitTimeLineModel();
this.mainTimeLine = new MainTimeLine(graphContainer, visualizations,
mainTimeLineModel, transitionCallback, resources);
// Graph overviews. Automatically monitors the MainTimeline.
this.overViewTimeLine = new OverViewTimeLine(graphContainer, mainTimeLine,
new OverViewTimeLineModel(), visualizations, resources);
// Create the DetailViews panel that will contain each DetailView.
this.detailsViewPanel = new DetailViews(container, resources);
// Create TimelineMarks for marking the timeline with vertical lines.
timelineMarks = new TimelineMarks(detailsViewPanel.getContainer(),
mainTimeLineModel.getGraphCalloutModel(), mainTimeLine, resources);
// Subscribe to page refreshes and load events.
initialState.getDataDispatcher().getTabChangeDispatcher().addListener(this);
initialState.getDataDispatcher().getUiEventDispatcher().addLoadEventListener(
this);
controller.observe(mainTimeLine, overViewTimeLine);
// Now load and populate the Visualization list.
createVisualizations(initialState, resources);
// Create the TabList. Stick it in before the timeline stuff.
TabList tabList = new TabList(new InsertingContainerImpl(
timeLineContainerElem, graphContainerElem));
refresh();
sinkEvents();
preventNativeSelection(tabList.getElement(), graphContainerElem);
}
public void clearTimelineMarks() {
timelineMarks.clear();
}
public MainTimeLine getMainTimeLine() {
return mainTimeLine;
}
/**
* Mark line on timeline when the DOM content is loaded.
*/
public void onDomContentLoaded(DomContentLoadedEvent event) {
timelineMarks.addMark(event.getTime(),
EventRecordColors.getColorForType(DomContentLoadedEvent.TYPE),
EventRecordType.typeToString(DomContentLoadedEvent.TYPE),
EventRecordType.typeToHelpString(DomContentLoadedEvent.TYPE), true);
timelineMarks.drawMarksInBounds(mainTimeLineModel.getLeftBound(),
mainTimeLineModel.getRightBound());
}
/**
* Page transitions can also be marked so that if we navigate back to a
* previous application state, we can see the point at which we tried to
* navigate away.
*/
public void onPageTransition(TabChangeEvent change) {
Url refresh = new Url(change.getUrl());
String resource = refresh.getLastPathComponent();
String description = "Navigating to "
+ (resource.equals("") ? refresh.getUrl() : resource);
timelineMarks.addMark(change.getTime(), Color.BLUE, description,
description, true);
timelineMarks.drawMarksInBounds(mainTimeLineModel.getLeftBound(),
mainTimeLineModel.getRightBound());
}
/**
* Mark line on timeline when we refresh a page.
*/
public void onRefresh(TabChangeEvent change) {
Url refresh = new Url(change.getUrl());
String resource = refresh.getLastPathComponent();
String description = "Refresh of "
+ (resource.equals("") ? refresh.getUrl() : resource);
timelineMarks.addMark(change.getTime(), Color.LIGHT_BLUE, description,
description, false);
timelineMarks.drawMarksInBounds(mainTimeLineModel.getLeftBound(),
mainTimeLineModel.getRightBound());
}
public void onWindowLoad(WindowLoadEvent event) {
timelineMarks.addMark(event.getTime(),
EventRecordColors.getColorForType(WindowLoadEvent.TYPE),
EventRecordType.typeToString(WindowLoadEvent.TYPE),
EventRecordType.typeToHelpString(WindowLoadEvent.TYPE), true);
timelineMarks.drawMarksInBounds(mainTimeLineModel.getLeftBound(),
mainTimeLineModel.getRightBound());
}
/**
* Sets the <code>loadedState</code> to the specified {@link ApplicationState}
* . It swaps in the new {@link VisualizationModel}s and reloads all the
* Visualizations and graph properties.
*
* @param state the {@link ApplicationState} we want to load
*/
public void setApplicationState(ApplicationState state) {
if (state.getLastDomainValue() > mainTimeLine.getModel().getMostRecentDomainValue()) {
mainTimeLineModel.onDomainChange(state.getLastDomainValue());
}
// Update all visualizations that have been loaded
for (int i = 0, n = visualizations.size(); i < n; i++) {
Visualization<?, ?> viz = visualizations.get(i);
viz.getModel().getGraphModel().removeDomainObserver(mainTimeLineModel);
VisualizationModel vizModel = state.getVisualizationModel(viz.getTitle());
viz.setModel(vizModel);
vizModel.getGraphModel().addDomainObserver(mainTimeLineModel);
}
loadedState = state;
mainTimeLineModel.updateBounds(state.getFirstDomainValue(),
state.getLastDomainValue());
overViewTimeLine.getModel().updateBounds(state.getFirstDomainValue(),
state.getLastDomainValue());
// Maybe redraw timeline marks.
timelineMarks.drawMarksInBounds(state.getFirstDomainValue(),
state.getLastDomainValue());
// Redraw a second frame so that it can rescale correctly
refresh();
}
private void createVisualizations(ApplicationState initialState,
MainTimeLine.Resources resources) {
// Sluggishness
SluggishnessModel sluggishnessModel = (SluggishnessModel) initialState.getVisualizationModel(SluggishnessVisualization.TITLE);
SluggishnessVisualization sluggishnessVisualization = new SluggishnessVisualization(
mainTimeLine, sluggishnessModel, detailsViewPanel.getContainer(),
resources);
// Network Visualization
NetworkVisualizationModel networkModel = (NetworkVisualizationModel) initialState.getVisualizationModel(NetworkVisualization.TITLE);
NetworkVisualization networkVisualization = new NetworkVisualization(
mainTimeLine, networkModel, detailsViewPanel.getContainer(), resources);
// Load the visualization that we just added.
loadVisualization(sluggishnessVisualization);
loadVisualization(networkVisualization);
// Setup the graphs to refresh when hintlet data arrives. Buffer the data
// so that the screen doesn't jump from rapid hintlet data coming in.
initialState.getDataDispatcher().getHintletEngineHost().addHintListener(
new HintletInterface.HintListener() {
boolean queued = false;
public void onHint(HintRecord hintlet) {
if (queued) {
return;
}
double hintletTime = hintlet.getTimestamp();
if (hintletTime < mainTimeLineModel.getLeftBound()
|| hintletTime > mainTimeLineModel.getRightBound()) {
// out of bounds - no need to refresh
return;
}
Timer t = new Timer() {
@Override
public void run() {
mainTimeLineModel.refresh();
queued = false;
}
};
t.schedule(HINTLET_REFRESH_DELAY_MS);
queued = true;
}
});
}
/**
* Updates the Y Axis label to be the max scale value for the currently
* selected visualization.
*/
private void fixYAxisLabel() {
mainTimeLine.updateYAxisLabel(
selectedVisualization.getGraphUiProps().getActiveMaxYAxisValue(),
selectedVisualization.getModel().getGraphModel().getYAxisUnit());
}
/**
* Adds a {@link Visualization} and its associated
* {@link com.google.speedtracer.client.view.DetailView}. It also hooks up the
* {@link MainTimeLine} to the underlying {@link GraphModel} associated with
* each Visualization.
*
* @param visualization
*/
private void loadVisualization(Visualization<?, ?> visualization) {
selectedVisualization = visualization;
visualizations.add(visualization);
detailsViewPanel.addViewForVisualization(visualization);
GraphModel graphModel = visualization.getModel().getGraphModel();
graphModel.addDomainObserver(mainTimeLineModel);
mainTimeLineModel.refresh();
fixYAxisLabel();
}
/**
* Simply stops ugly native selection on components surrounding our main
* graph. Accidental drags dirty the UI.
*/
private void preventNativeSelection(Element tabList,
DivElement graphContainerElem) {
MouseDownListener listener = new MouseDownListener() {
public void onMouseDown(MouseDownEvent event) {
event.preventDefault();
}
};
MouseDownEvent.addMouseDownListener(tabList, tabList, listener);
MouseDownEvent.addMouseDownListener(graphContainerElem, graphContainerElem,
listener);
}
private void refresh() {
mainTimeLineModel.refresh();
overViewTimeLine.refresh();
detailsViewPanel.updateCurrentView(mainTimeLineModel.getLeftBound(),
mainTimeLineModel.getRightBound());
}
/**
* Simple mechanism for setting the correct draw order for the visualizations
* in the list.
*
* @param moveToTop the visualization to move to the end.
*/
private void reOrderVisualizations(Visualization<?, ?> moveToTop) {
visualizations.remove(moveToTop);
visualizations.add(moveToTop);
}
private void sinkEvents() {
EventListener listener = new EventListener();
// WindowLevelEvents
ResizeEvent.addResizeListener(Window.get(), Window.get(), listener);
// TODO (jaimeyap): Re-implement jogging without using HotKey class.
}
}