// Copyright (c) 2011 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.
/**
* EventsView displays a filtered list of all events sharing a source, and
* a details pane for the selected sources.
*
* +----------------------++----------------+
* | filter box || |
* +----------------------+| |
* | || |
* | || |
* | || |
* | || |
* | source list || details |
* | || view |
* | || |
* | || |
* | || |
* | || |
* +----------------------++ |
* | action bar || |
* +----------------------++----------------+
*
* @constructor
*/
function EventsView(tableBodyId, filterInputId, filterCountId,
deleteSelectedId, deleteAllId, selectAllId, sortByIdId,
sortBySourceTypeId, sortByDescriptionId,
tabHandlesContainerId, logTabId, timelineTabId,
detailsLogBoxId, detailsTimelineBoxId,
topbarId, middleboxId, bottombarId, sizerId) {
View.call(this);
// Used for sorting entries with automatically assigned IDs.
this.maxReceivedSourceId_ = 0;
// Initialize the sub-views.
var leftPane = new TopMidBottomView(new DivView(topbarId),
new DivView(middleboxId),
new DivView(bottombarId));
this.detailsView_ = new DetailsView(tabHandlesContainerId,
logTabId,
timelineTabId,
detailsLogBoxId,
detailsTimelineBoxId);
this.splitterView_ = new ResizableVerticalSplitView(
leftPane, this.detailsView_, new DivView(sizerId));
g_browser.addLogObserver(this);
this.tableBody_ = document.getElementById(tableBodyId);
this.filterInput_ = document.getElementById(filterInputId);
this.filterCount_ = document.getElementById(filterCountId);
this.filterInput_.addEventListener('search',
this.onFilterTextChanged_.bind(this), true);
document.getElementById(deleteSelectedId).onclick =
this.deleteSelected_.bind(this);
document.getElementById(deleteAllId).onclick =
g_browser.deleteAllEvents.bind(g_browser);
document.getElementById(selectAllId).addEventListener(
'click', this.selectAll_.bind(this), true);
document.getElementById(sortByIdId).addEventListener(
'click', this.sortById_.bind(this), true);
document.getElementById(sortBySourceTypeId).addEventListener(
'click', this.sortBySourceType_.bind(this), true);
document.getElementById(sortByDescriptionId).addEventListener(
'click', this.sortByDescription_.bind(this), true);
// Sets sort order and filter.
this.setFilter_('');
this.initializeSourceList_();
}
inherits(EventsView, View);
/**
* Initializes the list of source entries. If source entries are already,
* being displayed, removes them all in the process.
*/
EventsView.prototype.initializeSourceList_ = function() {
this.currentSelectedSources_ = [];
this.sourceIdToEntryMap_ = {};
this.tableBody_.innerHTML = '';
this.numPrefilter_ = 0;
this.numPostfilter_ = 0;
this.invalidateFilterCounter_();
this.invalidateDetailsView_();
};
// How soon after updating the filter list the counter should be updated.
EventsView.REPAINT_FILTER_COUNTER_TIMEOUT_MS = 0;
EventsView.prototype.setGeometry = function(left, top, width, height) {
EventsView.superClass_.setGeometry.call(this, left, top, width, height);
this.splitterView_.setGeometry(left, top, width, height);
};
EventsView.prototype.show = function(isVisible) {
EventsView.superClass_.show.call(this, isVisible);
this.splitterView_.show(isVisible);
};
EventsView.prototype.getFilterText_ = function() {
return this.filterInput_.value;
};
EventsView.prototype.setFilterText_ = function(filterText) {
this.filterInput_.value = filterText;
this.onFilterTextChanged_();
};
EventsView.prototype.onFilterTextChanged_ = function() {
this.setFilter_(this.getFilterText_());
};
/**
* Updates text in the details view when security stripping is toggled.
*/
EventsView.prototype.onSecurityStrippingChanged = function() {
this.invalidateDetailsView_();
}
/**
* Sorts active entries first. If both entries are inactive, puts the one
* that was active most recently first. If both are active, uses source ID,
* which puts longer lived events at the top, and behaves better than using
* duration or time of first event.
*/
EventsView.compareActive_ = function(source1, source2) {
if (source1.isActive() && !source2.isActive())
return -1;
if (!source1.isActive() && source2.isActive())
return 1;
if (!source1.isActive()) {
var deltaEndTime = source1.getEndTime() - source2.getEndTime();
if (deltaEndTime != 0) {
// The one that ended most recently (Highest end time) should be sorted
// first.
return -deltaEndTime;
}
// If both ended at the same time, then odds are they were related events,
// started one after another, so sort in the opposite order of their
// source IDs to get a more intuitive ordering.
return -EventsView.compareSourceId_(source1, source2);
}
return EventsView.compareSourceId_(source1, source2);
};
EventsView.compareDescription_ = function(source1, source2) {
var source1Text = source1.getDescription().toLowerCase();
var source2Text = source2.getDescription().toLowerCase();
var compareResult = source1Text.localeCompare(source2Text);
if (compareResult != 0)
return compareResult;
return EventsView.compareSourceId_(source1, source2);
};
EventsView.compareDuration_ = function(source1, source2) {
var durationDifference = source2.getDuration() - source1.getDuration();
if (durationDifference)
return durationDifference;
return EventsView.compareSourceId_(source1, source2);
};
/**
* For the purposes of sorting by source IDs, entries without a source
* appear right after the SourceEntry with the highest source ID received
* before the sourceless entry. Any ambiguities are resolved by ordering
* the entries without a source by the order in which they were received.
*/
EventsView.compareSourceId_ = function(source1, source2) {
var sourceId1 = source1.getSourceId();
if (sourceId1 < 0)
sourceId1 = source1.getMaxPreviousEntrySourceId();
var sourceId2 = source2.getSourceId();
if (sourceId2 < 0)
sourceId2 = source2.getMaxPreviousEntrySourceId();
if (sourceId1 != sourceId2)
return sourceId1 - sourceId2;
// One or both have a negative ID. In either case, the source with the
// highest ID should be sorted first.
return source2.getSourceId() - source1.getSourceId();
};
EventsView.compareSourceType_ = function(source1, source2) {
var source1Text = source1.getSourceTypeString();
var source2Text = source2.getSourceTypeString();
var compareResult = source1Text.localeCompare(source2Text);
if (compareResult != 0)
return compareResult;
return EventsView.compareSourceId_(source1, source2);
};
EventsView.prototype.comparisonFuncWithReversing_ = function(a, b) {
var result = this.comparisonFunction_(a, b);
if (this.doSortBackwards_)
result *= -1;
return result;
};
EventsView.comparisonFunctionTable_ = {
// sort: and sort:- are allowed
'': EventsView.compareSourceId_,
'active': EventsView.compareActive_,
'desc': EventsView.compareDescription_,
'description': EventsView.compareDescription_,
'duration': EventsView.compareDuration_,
'id': EventsView.compareSourceId_,
'source': EventsView.compareSourceType_,
'type': EventsView.compareSourceType_
};
EventsView.prototype.Sort_ = function() {
var sourceEntries = [];
for (var id in this.sourceIdToEntryMap_) {
// Can only sort items with an actual row in the table.
if (this.sourceIdToEntryMap_[id].hasRow())
sourceEntries.push(this.sourceIdToEntryMap_[id]);
}
sourceEntries.sort(this.comparisonFuncWithReversing_.bind(this));
for (var i = sourceEntries.length - 2; i >= 0; --i) {
if (sourceEntries[i].getNextNodeSourceId() !=
sourceEntries[i + 1].getSourceId())
sourceEntries[i].moveBefore(sourceEntries[i + 1]);
}
};
/**
* Looks for the first occurence of |directive|:parameter in |sourceText|.
* Parameter can be an empty string.
*
* On success, returns an object with two fields:
* |remainingText| - |sourceText| with |directive|:parameter removed,
and excess whitespace deleted.
* |parameter| - the parameter itself.
*
* On failure, returns null.
*/
EventsView.prototype.parseDirective_ = function(sourceText, directive) {
// Adding a leading space allows a single regexp to be used, regardless of
// whether or not the directive is at the start of the string.
sourceText = ' ' + sourceText;
regExp = new RegExp('\\s+' + directive + ':(\\S*)\\s*', 'i');
matchInfo = regExp.exec(sourceText);
if (matchInfo == null)
return null;
return {'remainingText': sourceText.replace(regExp, ' ').trim(),
'parameter': matchInfo[1]};
};
/**
* Just like parseDirective_, except can optionally be a '-' before or
* the parameter, to negate it. Before is more natural, after
* allows more convenient toggling.
*
* Returned value has the additional field |isNegated|, and a leading
* '-' will be removed from |parameter|, if present.
*/
EventsView.prototype.parseNegatableDirective_ = function(sourceText,
directive) {
var matchInfo = this.parseDirective_(sourceText, directive);
if (matchInfo == null)
return null;
// Remove any leading or trailing '-' from the directive.
var negationInfo = /^(-?)(\S*?)$/.exec(matchInfo.parameter);
matchInfo.parameter = negationInfo[2];
matchInfo.isNegated = (negationInfo[1] == '-');
return matchInfo;
};
/**
* Parse any "sort:" directives, and update |comparisonFunction_| and
* |doSortBackwards_|as needed. Note only the last valid sort directive
* is used.
*
* Returns |filterText| with all sort directives removed, including
* invalid ones.
*/
EventsView.prototype.parseSortDirectives_ = function(filterText) {
this.comparisonFunction_ = EventsView.compareSourceId_;
this.doSortBackwards_ = false;
while (true) {
var sortInfo = this.parseNegatableDirective_(filterText, 'sort');
if (sortInfo == null)
break;
var comparisonName = sortInfo.parameter.toLowerCase();
if (EventsView.comparisonFunctionTable_[comparisonName] != null) {
this.comparisonFunction_ =
EventsView.comparisonFunctionTable_[comparisonName];
this.doSortBackwards_ = sortInfo.isNegated;
}
filterText = sortInfo.remainingText;
}
return filterText;
};
/**
* Parse any "is:" directives, and update |filter| accordingly.
*
* Returns |filterText| with all "is:" directives removed, including
* invalid ones.
*/
EventsView.prototype.parseRestrictDirectives_ = function(filterText, filter) {
while (true) {
var filterInfo = this.parseNegatableDirective_(filterText, 'is');
if (filterInfo == null)
break;
if (filterInfo.parameter == 'active') {
if (!filterInfo.isNegated)
filter.isActive = true;
else
filter.isInactive = true;
}
filterText = filterInfo.remainingText;
}
return filterText;
};
/**
* Parses all directives that take arbitrary strings as input,
* and updates |filter| accordingly. Directives of these types
* are stored as lists.
*
* Returns |filterText| with all recognized directives removed.
*/
EventsView.prototype.parseStringDirectives_ = function(filterText, filter) {
var directives = ['type', 'id'];
for (var i = 0; i < directives.length; ++i) {
while (true) {
var directive = directives[i];
var filterInfo = this.parseDirective_(filterText, directive);
if (filterInfo == null)
break;
if (!filter[directive])
filter[directive] = [];
filter[directive].push(filterInfo.parameter);
filterText = filterInfo.remainingText;
}
}
return filterText;
};
/*
* Converts |filterText| into an object representing the filter.
*/
EventsView.prototype.createFilter_ = function(filterText) {
var filter = {};
filterText = filterText.toLowerCase();
filterText = this.parseRestrictDirectives_(filterText, filter);
filterText = this.parseStringDirectives_(filterText, filter);
filter.text = filterText.trim();
return filter;
};
EventsView.prototype.setFilter_ = function(filterText) {
var lastComparisonFunction = this.comparisonFunction_;
var lastDoSortBackwards = this.doSortBackwards_;
filterText = this.parseSortDirectives_(filterText);
if (lastComparisonFunction != this.comparisonFunction_ ||
lastDoSortBackwards != this.doSortBackwards_) {
this.Sort_();
}
this.currentFilter_ = this.createFilter_(filterText);
// Iterate through all of the rows and see if they match the filter.
for (var id in this.sourceIdToEntryMap_) {
var entry = this.sourceIdToEntryMap_[id];
entry.setIsMatchedByFilter(entry.matchesFilter(this.currentFilter_));
}
};
/**
* Repositions |sourceEntry|'s row in the table using an insertion sort.
* Significantly faster than sorting the entire table again, when only
* one entry has changed.
*/
EventsView.prototype.InsertionSort_ = function(sourceEntry) {
// SourceEntry that should be after |sourceEntry|, if it needs
// to be moved earlier in the list.
var sourceEntryAfter = sourceEntry;
while (true) {
var prevSourceId = sourceEntryAfter.getPreviousNodeSourceId();
if (prevSourceId == null)
break;
var prevSourceEntry = this.sourceIdToEntryMap_[prevSourceId];
if (this.comparisonFuncWithReversing_(sourceEntry, prevSourceEntry) >= 0)
break;
sourceEntryAfter = prevSourceEntry;
}
if (sourceEntryAfter != sourceEntry) {
sourceEntry.moveBefore(sourceEntryAfter);
return;
}
var sourceEntryBefore = sourceEntry;
while (true) {
var nextSourceId = sourceEntryBefore.getNextNodeSourceId();
if (nextSourceId == null)
break;
var nextSourceEntry = this.sourceIdToEntryMap_[nextSourceId];
if (this.comparisonFuncWithReversing_(sourceEntry, nextSourceEntry) <= 0)
break;
sourceEntryBefore = nextSourceEntry;
}
if (sourceEntryBefore != sourceEntry)
sourceEntry.moveAfter(sourceEntryBefore);
};
EventsView.prototype.onLogEntryAdded = function(logEntry) {
var id = logEntry.source.id;
// Lookup the source.
var sourceEntry = this.sourceIdToEntryMap_[id];
if (!sourceEntry) {
sourceEntry = new SourceEntry(this, this.maxReceivedSourceId_);
this.sourceIdToEntryMap_[id] = sourceEntry;
this.incrementPrefilterCount(1);
if (id > this.maxReceivedSourceId_)
this.maxReceivedSourceId_ = id;
}
sourceEntry.update(logEntry);
if (sourceEntry.isSelected())
this.invalidateDetailsView_();
// TODO(mmenke): Fix sorting when sorting by duration.
// Duration continuously increases for all entries that are
// still active. This can result in incorrect sorting, until
// Sort_ is called.
this.InsertionSort_(sourceEntry);
};
/**
* Returns the SourceEntry with the specified ID, if there is one.
* Otherwise, returns undefined.
*/
EventsView.prototype.getSourceEntry = function(id) {
return this.sourceIdToEntryMap_[id];
};
/**
* Called whenever some log events are deleted. |sourceIds| lists
* the source IDs of all deleted log entries.
*/
EventsView.prototype.onLogEntriesDeleted = function(sourceIds) {
for (var i = 0; i < sourceIds.length; ++i) {
var id = sourceIds[i];
var entry = this.sourceIdToEntryMap_[id];
if (entry) {
entry.remove();
delete this.sourceIdToEntryMap_[id];
this.incrementPrefilterCount(-1);
}
}
};
/**
* Called whenever all log events are deleted.
*/
EventsView.prototype.onAllLogEntriesDeleted = function() {
this.initializeSourceList_();
};
/**
* Called when either a log file is loaded or when going back to actively
* logging events. In either case, called after clearing the old entries,
* but before getting any new ones.
*/
EventsView.prototype.onSetIsViewingLogFile = function(isViewingLogFile) {
// Needed to sort new sourceless entries correctly.
this.maxReceivedSourceId_ = 0;
};
EventsView.prototype.incrementPrefilterCount = function(offset) {
this.numPrefilter_ += offset;
this.invalidateFilterCounter_();
};
EventsView.prototype.incrementPostfilterCount = function(offset) {
this.numPostfilter_ += offset;
this.invalidateFilterCounter_();
};
EventsView.prototype.onSelectionChanged = function() {
this.invalidateDetailsView_();
};
EventsView.prototype.clearSelection = function() {
var prevSelection = this.currentSelectedSources_;
this.currentSelectedSources_ = [];
// Unselect everything that is currently selected.
for (var i = 0; i < prevSelection.length; ++i) {
prevSelection[i].setSelected(false);
}
this.onSelectionChanged();
};
EventsView.prototype.deleteSelected_ = function() {
var sourceIds = [];
for (var i = 0; i < this.currentSelectedSources_.length; ++i) {
var entry = this.currentSelectedSources_[i];
sourceIds.push(entry.getSourceId());
}
g_browser.deleteEventsBySourceId(sourceIds);
};
EventsView.prototype.selectAll_ = function(event) {
for (var id in this.sourceIdToEntryMap_) {
var entry = this.sourceIdToEntryMap_[id];
if (entry.isMatchedByFilter()) {
entry.setSelected(true);
}
}
event.preventDefault();
};
EventsView.prototype.unselectAll_ = function() {
var entries = this.currentSelectedSources_.slice(0);
for (var i = 0; i < entries.length; ++i) {
entries[i].setSelected(false);
}
};
/**
* If |params| includes a query, replaces the current filter and unselects.
* all items.
*/
EventsView.prototype.setParameters = function(params) {
if (params.q) {
this.unselectAll_();
this.setFilterText_(params.q);
}
};
/**
* If already using the specified sort method, flips direction. Otherwise,
* removes pre-existing sort parameter before adding the new one.
*/
EventsView.prototype.toggleSortMethod_ = function(sortMethod) {
// Remove old sort directives, if any.
var filterText = this.parseSortDirectives_(this.getFilterText_());
// If already using specified sortMethod, sort backwards.
if (!this.doSortBackwards_ &&
EventsView.comparisonFunctionTable_[sortMethod] ==
this.comparisonFunction_)
sortMethod = '-' + sortMethod;
filterText = 'sort:' + sortMethod + ' ' + filterText;
this.setFilterText_(filterText.trim());
};
EventsView.prototype.sortById_ = function(event) {
this.toggleSortMethod_('id');
};
EventsView.prototype.sortBySourceType_ = function(event) {
this.toggleSortMethod_('source');
};
EventsView.prototype.sortByDescription_ = function(event) {
this.toggleSortMethod_('desc');
};
EventsView.prototype.modifySelectionArray = function(
sourceEntry, addToSelection) {
// Find the index for |sourceEntry| in the current selection list.
var index = -1;
for (var i = 0; i < this.currentSelectedSources_.length; ++i) {
if (this.currentSelectedSources_[i] == sourceEntry) {
index = i;
break;
}
}
if (index != -1 && !addToSelection) {
// Remove from the selection.
this.currentSelectedSources_.splice(index, 1);
}
if (index == -1 && addToSelection) {
this.currentSelectedSources_.push(sourceEntry);
}
};
EventsView.prototype.invalidateDetailsView_ = function() {
this.detailsView_.setData(this.currentSelectedSources_);
};
EventsView.prototype.invalidateFilterCounter_ = function() {
if (!this.outstandingRepaintFilterCounter_) {
this.outstandingRepaintFilterCounter_ = true;
window.setTimeout(this.repaintFilterCounter_.bind(this),
EventsView.REPAINT_FILTER_COUNTER_TIMEOUT_MS);
}
};
EventsView.prototype.repaintFilterCounter_ = function() {
this.outstandingRepaintFilterCounter_ = false;
this.filterCount_.innerHTML = '';
addTextNode(this.filterCount_,
this.numPostfilter_ + ' of ' + this.numPrefilter_);
};