// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
'use strict';
/**
* @fileoverview TimelineView visualizes TRACE_EVENT events using the
* tracing.Timeline component and adds in selection summary and control buttons.
*/
cr.define('tracing', function() {
/**
* TimelineFindControl
* @constructor
* @extends {tracing.Overlay}
*/
var TimelineFindControl = cr.ui.define('div');
TimelineFindControl.prototype = {
__proto__: tracing.Overlay.prototype,
decorate: function() {
tracing.Overlay.prototype.decorate.call(this);
this.className = 'timeline-find-control';
this.hitCountEl_ = document.createElement('div');
this.hitCountEl_.className = 'hit-count-label';
this.hitCountEl_.textContent = '1 of 7';
var findPreviousBn = document.createElement('div');
findPreviousBn.className = 'timeline-button find-previous';
findPreviousBn.textContent = '\u2190';
findPreviousBn.addEventListener('click', function() {
this.controller.findPrevious();
this.updateHitCountEl_();
}.bind(this));
var findNextBn = document.createElement('div');
findNextBn.className = 'timeline-button find-next';
findNextBn.textContent = '\u2192';
findNextBn.addEventListener('click', function() {
this.controller.findNext();
this.updateHitCountEl_();
}.bind(this));
// Filter input element.
this.filterEl_ = document.createElement('input');
this.filterEl_.type = 'input';
this.filterEl_.addEventListener('input', function(e) {
this.controller.filterText = this.filterEl_.value;
this.updateHitCountEl_();
}.bind(this));
this.filterEl_.addEventListener('keydown', function(e) {
if (e.keyCode == 13) {
findNextBn.click();
} else if (e.keyCode == 27) {
this.filterEl_.blur();
this.updateHitCountEl_();
}
}.bind(this));
this.filterEl_.addEventListener('blur', function(e) {
this.updateHitCountEl_();
}.bind(this));
this.filterEl_.addEventListener('focus', function(e) {
this.updateHitCountEl_();
}.bind(this));
// Attach everything.
this.appendChild(this.filterEl_);
this.appendChild(findPreviousBn);
this.appendChild(findNextBn);
this.appendChild(this.hitCountEl_);
this.updateHitCountEl_();
},
get controller() {
return this.controller_;
},
set controller(c) {
this.controller_ = c;
this.updateHitCountEl_();
},
focus: function() {
this.filterEl_.selectionStart = 0;
this.filterEl_.selectionEnd = this.filterEl_.value.length;
this.filterEl_.focus();
},
updateHitCountEl_: function() {
if (!this.controller || document.activeElement != this.filterEl_) {
this.hitCountEl_.textContent = '';
return;
}
var i = this.controller.currentHitIndex;
var n = this.controller.filterHits.length;
if (n == 0)
this.hitCountEl_.textContent = '0 of 0';
else
this.hitCountEl_.textContent = (i + 1) + ' of ' + n;
}
};
function TimelineFindController() {
this.timeline_ = undefined;
this.model_ = undefined;
this.filterText_ = '';
this.filterHits_ = new tracing.TimelineSelection();
this.filterHitsDirty_ = true;
this.currentHitIndex_ = 0;
};
TimelineFindController.prototype = {
__proto__: Object.prototype,
get timeline() {
return this.timeline_;
},
set timeline(t) {
this.timeline_ = t;
this.filterHitsDirty_ = true;
},
get filterText() {
return this.filterText_;
},
set filterText(f) {
if (f == this.filterText_)
return;
this.filterText_ = f;
this.filterHitsDirty_ = true;
this.findNext();
},
get filterHits() {
if (this.filterHitsDirty_) {
this.filterHitsDirty_ = false;
if (this.timeline_) {
var filter = new tracing.TimelineFilter(this.filterText);
this.filterHits_.clear();
this.timeline.addAllObjectsMatchingFilterToSelection(
filter, this.filterHits_);
this.currentHitIndex_ = this.filterHits_.length - 1;
} else {
this.filterHits_.clear();
this.currentHitIndex_ = 0;
}
}
return this.filterHits_;
},
get currentHitIndex() {
return this.currentHitIndex_;
},
find_: function(dir) {
if (!this.timeline)
return;
var N = this.filterHits.length;
this.currentHitIndex_ = this.currentHitIndex_ + dir;
if (this.currentHitIndex_ < 0) this.currentHitIndex_ = N - 1;
if (this.currentHitIndex_ >= N) this.currentHitIndex_ = 0;
if (this.currentHitIndex_ < 0 || this.currentHitIndex_ >= N) {
this.timeline.selection = new tracing.TimelineSelection();
return;
}
// We allow the zoom level to change on the first hit level. But, when
// then cycling through subsequent changes, restrict it to panning.
var zoomAllowed = this.currentHitIndex_ == 0;
var subSelection = this.filterHits.subSelection(this.currentHitIndex_);
this.timeline.setSelectionAndMakeVisible(subSelection, zoomAllowed);
},
findNext: function() {
this.find_(1);
},
findPrevious: function() {
this.find_(-1);
},
};
/**
* TimelineView
* @constructor
* @extends {HTMLDivElement}
*/
var TimelineView = cr.ui.define('div');
TimelineView.prototype = {
__proto__: HTMLDivElement.prototype,
decorate: function() {
this.classList.add('timeline-view');
// Create individual elements.
this.titleEl_ = document.createElement('div');
this.titleEl_.textContent = 'Tracing: ';
this.controlDiv_ = document.createElement('div');
this.controlDiv_.className = 'control';
this.leftControlsEl_ = document.createElement('div');
this.leftControlsEl_.className = 'controls';
this.rightControlsEl_ = document.createElement('div');
this.rightControlsEl_.className = 'controls';
var spacingEl = document.createElement('div');
spacingEl.className = 'spacer';
this.timelineContainer_ = document.createElement('div');
this.timelineContainer_.className = 'timeline-container';
var analysisContainer_ = document.createElement('div');
analysisContainer_.className = 'analysis-container';
this.analysisEl_ = new tracing.TimelineAnalysisView();
this.findCtl_ = new TimelineFindControl();
this.findCtl_.controller = new TimelineFindController();
// Connect everything up.
this.rightControls.appendChild(this.findCtl_);
this.controlDiv_.appendChild(this.titleEl_);
this.controlDiv_.appendChild(this.leftControlsEl_);
this.controlDiv_.appendChild(spacingEl);
this.controlDiv_.appendChild(this.rightControlsEl_);
this.appendChild(this.controlDiv_);
this.appendChild(this.timelineContainer_);
analysisContainer_.appendChild(this.analysisEl_);
this.appendChild(analysisContainer_);
this.rightControls.appendChild(this.createHelpButton_());
// Bookkeeping.
this.onSelectionChangedBoundToThis_ = this.onSelectionChanged_.bind(this);
document.addEventListener('keypress', this.onKeypress_.bind(this), true);
},
createHelpButton_: function() {
var dlg = new tracing.Overlay();
dlg.classList.add('timeline-view-help-overlay');
var showHelpEl = document.createElement('div');
showHelpEl.className = 'timeline-button timeline-view-help-button';
showHelpEl.textContent = '?';
var helpTextEl = document.createElement('div');
helpTextEl.style.whiteSpace = 'pre';
helpTextEl.style.fontFamily = 'monospace';
function onClick() {
dlg.visible = true;
helpTextEl.textContent = this.timeline_.keyHelp;
document.addEventListener('keydown', onKey, true);
}
function onKey(e) {
if (!dlg.visible)
return;
if (e.keyCode == 27 || e.keyCode == '?'.charCodeAt(0)) {
e.preventDefault();
document.removeEventListener('keydown', onKey);
dlg.visible = false;
}
}
showHelpEl.addEventListener('click', onClick.bind(this));
dlg.appendChild(helpTextEl);
return showHelpEl;
},
get leftControls() {
return this.leftControlsEl_;
},
get rightControls() {
return this.rightControlsEl_;
},
get title() {
return this.titleEl_.textContent.substring(
this.titleEl_.textContent.length - 2);
},
set title(text) {
this.titleEl_.textContent = text + ':';
},
set traceData(traceData) {
this.model = new tracing.TimelineModel(traceData);
},
get model() {
return this.timelineModel_;
},
set model(model) {
this.timelineModel_ = model;
// remove old timeline
this.timelineContainer_.textContent = '';
// create new timeline if needed
if (this.timelineModel_.minTimestamp !== undefined) {
if (this.timeline_) {
this.timeline_.viewportTrack.detach();
this.timeline_.detach();
}
this.timeline_ = new tracing.Timeline();
this.timeline_.model = this.timelineModel_;
this.timeline_.focusElement =
this.focusElement_ ? this.focusElement_ : this.parentElement;
this.insertBefore(this.timeline_.viewportTrack, this.timelineContainer_);
this.timelineContainer_.appendChild(this.timeline_);
this.timeline_.addEventListener('selectionChange',
this.onSelectionChangedBoundToThis_);
this.findCtl_.controller.timeline = this.timeline_;
this.onSelectionChanged_();
} else {
this.timeline_ = undefined;
this.findCtl_.controller.timeline = undefined;
}
},
get timeline() {
return this.timeline_;
},
/**
* Sets the element whose focus state will determine whether
* to respond to keybaord input.
*/
set focusElement(value) {
this.focusElement_ = value;
if (this.timeline_)
this.timeline_.focusElement = value;
},
/**
* @return {Element} The element whose focused state determines
* whether to respond to keyboard inputs.
* Defaults to the parent element.
*/
get focusElement() {
if (this.focusElement_)
return this.focusElement_;
return this.parentElement;
},
/**
* @return {boolean} Whether the current timeline is attached to the
* document.
*/
get isAttachedToDocument_() {
var cur = this;
while (cur.parentNode)
cur = cur.parentNode;
return cur == this.ownerDocument;
},
get listenToKeys_() {
if (!this.isAttachedToDocument_)
return;
if (!this.focusElement_)
return true;
if (this.focusElement.tabIndex >= 0)
return document.activeElement == this.focusElement;
return true;
},
onKeypress_: function(e) {
if (!this.listenToKeys_)
return;
if (event.keyCode == '/'.charCodeAt(0)) { // / key
this.findCtl_.focus();
event.preventDefault();
return;
} else if (e.keyCode == '?'.charCodeAt(0)) {
this.querySelector('.timeline-view-help-button').click();
e.preventDefault();
}
},
beginFind: function() {
if (this.findInProgress_)
return;
this.findInProgress_ = true;
var dlg = TimelineFindControl();
dlg.controller = new TimelineFindController();
dlg.controller.timeline = this.timeline;
dlg.visible = true;
dlg.addEventListener('close', function() {
this.findInProgress_ = false;
}.bind(this));
dlg.addEventListener('findNext', function() {
});
dlg.addEventListener('findPrevious', function() {
});
},
onSelectionChanged_: function(e) {
var oldScrollTop = this.timelineContainer_.scrollTop;
this.analysisEl_.selection = this.timeline_.selection;
this.timelineContainer_.scrollTop = oldScrollTop;
}
};
return {
TimelineFindControl: TimelineFindControl,
TimelineFindController: TimelineFindController,
TimelineView: TimelineView
};
});