1 // Copyright (c) 2012 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
6 * EventsView displays a filtered list of all events sharing a source, and
7 * a details pane for the selected sources.
9 * +----------------------++----------------+
11 * +----------------------+| |
16 * | source list || details |
24 * +----------------------++----------------+
26 var EventsView = (function() {
29 // How soon after updating the filter list the counter should be updated.
30 var REPAINT_FILTER_COUNTER_TIMEOUT_MS = 0;
32 // We inherit from View.
33 var superClass = View;
38 function EventsView() {
39 assertFirstConstructorCall(EventsView);
41 // Call superclass's constructor.
42 superClass.call(this);
44 // Initialize the sub-views.
45 var leftPane = new VerticalSplitView(new DivView(EventsView.TOPBAR_ID),
46 new DivView(EventsView.LIST_BOX_ID));
48 this.detailsView_ = new DetailsView(EventsView.DETAILS_LOG_BOX_ID);
50 this.splitterView_ = new ResizableVerticalSplitView(
51 leftPane, this.detailsView_, new DivView(EventsView.SIZER_ID));
53 SourceTracker.getInstance().addSourceEntryObserver(this);
55 this.tableBody_ = $(EventsView.TBODY_ID);
57 this.filterInput_ = $(EventsView.FILTER_INPUT_ID);
58 this.filterCount_ = $(EventsView.FILTER_COUNT_ID);
60 this.filterInput_.addEventListener('search',
61 this.onFilterTextChanged_.bind(this), true);
63 $(EventsView.SELECT_ALL_ID).addEventListener(
64 'click', this.selectAll_.bind(this), true);
66 $(EventsView.SORT_BY_ID_ID).addEventListener(
67 'click', this.sortById_.bind(this), true);
69 $(EventsView.SORT_BY_SOURCE_TYPE_ID).addEventListener(
70 'click', this.sortBySourceType_.bind(this), true);
72 $(EventsView.SORT_BY_DESCRIPTION_ID).addEventListener(
73 'click', this.sortByDescription_.bind(this), true);
75 new MouseOverHelp(EventsView.FILTER_HELP_ID,
76 EventsView.FILTER_HELP_HOVER_ID);
78 // Sets sort order and filter.
81 this.initializeSourceList_();
84 EventsView.TAB_ID = 'tab-handle-events';
85 EventsView.TAB_NAME = 'Events';
86 EventsView.TAB_HASH = '#events';
88 // IDs for special HTML elements in events_view.html
89 EventsView.TBODY_ID = 'events-view-source-list-tbody';
90 EventsView.FILTER_INPUT_ID = 'events-view-filter-input';
91 EventsView.FILTER_COUNT_ID = 'events-view-filter-count';
92 EventsView.FILTER_HELP_ID = 'events-view-filter-help';
93 EventsView.FILTER_HELP_HOVER_ID = 'events-view-filter-help-hover';
94 EventsView.SELECT_ALL_ID = 'events-view-select-all';
95 EventsView.SORT_BY_ID_ID = 'events-view-sort-by-id';
96 EventsView.SORT_BY_SOURCE_TYPE_ID = 'events-view-sort-by-source';
97 EventsView.SORT_BY_DESCRIPTION_ID = 'events-view-sort-by-description';
98 EventsView.DETAILS_LOG_BOX_ID = 'events-view-details-log-box';
99 EventsView.TOPBAR_ID = 'events-view-filter-box';
100 EventsView.LIST_BOX_ID = 'events-view-source-list';
101 EventsView.SIZER_ID = 'events-view-splitter-box';
103 cr.addSingletonGetter(EventsView);
105 EventsView.prototype = {
106 // Inherit the superclass's methods.
107 __proto__: superClass.prototype,
110 * Initializes the list of source entries. If source entries are already,
111 * being displayed, removes them all in the process.
113 initializeSourceList_: function() {
114 this.currentSelectedRows_ = [];
115 this.sourceIdToRowMap_ = {};
116 this.tableBody_.innerHTML = '';
117 this.numPrefilter_ = 0;
118 this.numPostfilter_ = 0;
119 this.invalidateFilterCounter_();
120 this.invalidateDetailsView_();
123 setGeometry: function(left, top, width, height) {
124 superClass.prototype.setGeometry.call(this, left, top, width, height);
125 this.splitterView_.setGeometry(left, top, width, height);
128 show: function(isVisible) {
129 superClass.prototype.show.call(this, isVisible);
130 this.splitterView_.show(isVisible);
133 getFilterText_: function() {
134 return this.filterInput_.value;
137 setFilterText_: function(filterText) {
138 this.filterInput_.value = filterText;
139 this.onFilterTextChanged_();
142 onFilterTextChanged_: function() {
143 this.setFilter_(this.getFilterText_());
147 * Updates text in the details view when privacy stripping is toggled.
149 onPrivacyStrippingChanged: function() {
150 this.invalidateDetailsView_();
153 comparisonFuncWithReversing_: function(a, b) {
154 var result = this.comparisonFunction_(a, b);
155 if (this.doSortBackwards_)
161 var sourceEntries = [];
162 for (var id in this.sourceIdToRowMap_) {
163 sourceEntries.push(this.sourceIdToRowMap_[id].getSourceEntry());
165 sourceEntries.sort(this.comparisonFuncWithReversing_.bind(this));
167 // Reposition source rows from back to front.
168 for (var i = sourceEntries.length - 2; i >= 0; --i) {
169 var sourceRow = this.sourceIdToRowMap_[sourceEntries[i].getSourceId()];
170 var nextSourceId = sourceEntries[i + 1].getSourceId();
171 if (sourceRow.getNextNodeSourceId() != nextSourceId) {
172 var nextSourceRow = this.sourceIdToRowMap_[nextSourceId];
173 sourceRow.moveBefore(nextSourceRow);
178 setFilter_: function(filterText) {
179 var lastComparisonFunction = this.comparisonFunction_;
180 var lastDoSortBackwards = this.doSortBackwards_;
182 var filterParser = new SourceFilterParser(filterText);
183 this.currentFilter_ = filterParser.filter;
185 this.pickSortFunction_(filterParser.sort);
187 if (lastComparisonFunction != this.comparisonFunction_ ||
188 lastDoSortBackwards != this.doSortBackwards_) {
192 // Iterate through all of the rows and see if they match the filter.
193 for (var id in this.sourceIdToRowMap_) {
194 var entry = this.sourceIdToRowMap_[id];
195 entry.setIsMatchedByFilter(this.currentFilter_(entry.getSourceEntry()));
200 * Given a "sort" object with "method" and "backwards" keys, looks up and
201 * sets |comparisonFunction_| and |doSortBackwards_|. If the ID does not
202 * correspond to a sort function, defaults to sorting by ID.
204 pickSortFunction_: function(sort) {
205 this.doSortBackwards_ = sort.backwards;
206 this.comparisonFunction_ = COMPARISON_FUNCTION_TABLE[sort.method];
207 if (!this.comparisonFunction_) {
208 this.doSortBackwards_ = false;
209 this.comparisonFunction_ = compareSourceId_;
214 * Repositions |sourceRow|'s in the table using an insertion sort.
215 * Significantly faster than sorting the entire table again, when only
216 * one entry has changed.
218 insertionSort_: function(sourceRow) {
219 // SourceRow that should be after |sourceRow|, if it needs
220 // to be moved earlier in the list.
221 var sourceRowAfter = sourceRow;
223 var prevSourceId = sourceRowAfter.getPreviousNodeSourceId();
224 if (prevSourceId == null)
226 var prevSourceRow = this.sourceIdToRowMap_[prevSourceId];
227 if (this.comparisonFuncWithReversing_(
228 sourceRow.getSourceEntry(),
229 prevSourceRow.getSourceEntry()) >= 0) {
232 sourceRowAfter = prevSourceRow;
234 if (sourceRowAfter != sourceRow) {
235 sourceRow.moveBefore(sourceRowAfter);
239 var sourceRowBefore = sourceRow;
241 var nextSourceId = sourceRowBefore.getNextNodeSourceId();
242 if (nextSourceId == null)
244 var nextSourceRow = this.sourceIdToRowMap_[nextSourceId];
245 if (this.comparisonFuncWithReversing_(
246 sourceRow.getSourceEntry(),
247 nextSourceRow.getSourceEntry()) <= 0) {
250 sourceRowBefore = nextSourceRow;
252 if (sourceRowBefore != sourceRow)
253 sourceRow.moveAfter(sourceRowBefore);
257 * Called whenever SourceEntries are updated with new log entries. Updates
258 * the corresponding table rows, sort order, and the details view as needed.
260 onSourceEntriesUpdated: function(sourceEntries) {
261 var isUpdatedSourceSelected = false;
262 var numNewSourceEntries = 0;
264 for (var i = 0; i < sourceEntries.length; ++i) {
265 var sourceEntry = sourceEntries[i];
268 var sourceRow = this.sourceIdToRowMap_[sourceEntry.getSourceId()];
271 sourceRow = new SourceRow(this, sourceEntry);
272 this.sourceIdToRowMap_[sourceEntry.getSourceId()] = sourceRow;
273 ++numNewSourceEntries;
275 sourceRow.onSourceUpdated();
278 if (sourceRow.isSelected())
279 isUpdatedSourceSelected = true;
281 // TODO(mmenke): Fix sorting when sorting by duration.
282 // Duration continuously increases for all entries that
283 // are still active. This can result in incorrect
284 // sorting, until sort_ is called.
285 this.insertionSort_(sourceRow);
288 if (isUpdatedSourceSelected)
289 this.invalidateDetailsView_();
290 if (numNewSourceEntries)
291 this.incrementPrefilterCount(numNewSourceEntries);
295 * Returns the SourceRow with the specified ID, if there is one.
296 * Otherwise, returns undefined.
298 getSourceRow: function(id) {
299 return this.sourceIdToRowMap_[id];
303 * Called whenever all log events are deleted.
305 onAllSourceEntriesDeleted: function() {
306 this.initializeSourceList_();
310 * Called when either a log file is loaded, after clearing the old entries,
311 * but before getting any new ones.
313 onLoadLogStart: function() {
314 // Needed to sort new sourceless entries correctly.
315 this.maxReceivedSourceId_ = 0;
318 onLoadLogFinish: function(data) {
322 incrementPrefilterCount: function(offset) {
323 this.numPrefilter_ += offset;
324 this.invalidateFilterCounter_();
327 incrementPostfilterCount: function(offset) {
328 this.numPostfilter_ += offset;
329 this.invalidateFilterCounter_();
332 onSelectionChanged: function() {
333 this.invalidateDetailsView_();
336 clearSelection: function() {
337 var prevSelection = this.currentSelectedRows_;
338 this.currentSelectedRows_ = [];
340 // Unselect everything that is currently selected.
341 for (var i = 0; i < prevSelection.length; ++i) {
342 prevSelection[i].setSelected(false);
345 this.onSelectionChanged();
348 selectAll_: function(event) {
349 for (var id in this.sourceIdToRowMap_) {
350 var sourceRow = this.sourceIdToRowMap_[id];
351 if (sourceRow.isMatchedByFilter()) {
352 sourceRow.setSelected(true);
355 event.preventDefault();
358 unselectAll_: function() {
359 var entries = this.currentSelectedRows_.slice(0);
360 for (var i = 0; i < entries.length; ++i) {
361 entries[i].setSelected(false);
366 * If |params| includes a query, replaces the current filter and unselects.
367 * all items. If it includes a selection, tries to select the relevant
370 setParameters: function(params) {
373 this.setFilterText_(params.q);
377 var sourceRow = this.sourceIdToRowMap_[params.s];
379 sourceRow.setSelected(true);
380 this.scrollToSourceId(params.s);
386 * Scrolls to the source indicated by |sourceId|, if displayed.
388 scrollToSourceId: function(sourceId) {
389 this.detailsView_.scrollToSourceId(sourceId);
393 * If already using the specified sort method, flips direction. Otherwise,
394 * removes pre-existing sort parameter before adding the new one.
396 toggleSortMethod_: function(sortMethod) {
397 // Get old filter text and remove old sort directives, if any.
398 var filterParser = new SourceFilterParser(this.getFilterText_());
399 var filterText = filterParser.filterTextWithoutSort;
401 filterText = 'sort:' + sortMethod + ' ' + filterText;
403 // If already using specified sortMethod, sort backwards.
404 if (!this.doSortBackwards_ &&
405 COMPARISON_FUNCTION_TABLE[sortMethod] == this.comparisonFunction_) {
406 filterText = '-' + filterText;
409 this.setFilterText_(filterText.trim());
412 sortById_: function(event) {
413 this.toggleSortMethod_('id');
416 sortBySourceType_: function(event) {
417 this.toggleSortMethod_('source');
420 sortByDescription_: function(event) {
421 this.toggleSortMethod_('desc');
425 * Modifies the map of selected rows to include/exclude the one with
426 * |sourceId|, if present. Does not modify checkboxes or the LogView.
427 * Should only be called by a SourceRow in response to its selection
430 modifySelectionArray: function(sourceId, addToSelection) {
431 var sourceRow = this.sourceIdToRowMap_[sourceId];
434 // Find the index for |sourceEntry| in the current selection list.
436 for (var i = 0; i < this.currentSelectedRows_.length; ++i) {
437 if (this.currentSelectedRows_[i] == sourceRow) {
443 if (index != -1 && !addToSelection) {
444 // Remove from the selection.
445 this.currentSelectedRows_.splice(index, 1);
448 if (index == -1 && addToSelection) {
449 this.currentSelectedRows_.push(sourceRow);
453 getSelectedSourceEntries_: function() {
454 var sourceEntries = [];
455 for (var i = 0; i < this.currentSelectedRows_.length; ++i) {
456 sourceEntries.push(this.currentSelectedRows_[i].getSourceEntry());
458 return sourceEntries;
461 invalidateDetailsView_: function() {
462 this.detailsView_.setData(this.getSelectedSourceEntries_());
465 invalidateFilterCounter_: function() {
466 if (!this.outstandingRepaintFilterCounter_) {
467 this.outstandingRepaintFilterCounter_ = true;
468 window.setTimeout(this.repaintFilterCounter_.bind(this),
469 REPAINT_FILTER_COUNTER_TIMEOUT_MS);
473 repaintFilterCounter_: function() {
474 this.outstandingRepaintFilterCounter_ = false;
475 this.filterCount_.innerHTML = '';
476 addTextNode(this.filterCount_,
477 this.numPostfilter_ + ' of ' + this.numPrefilter_);
479 }; // end of prototype.
481 // ------------------------------------------------------------------------
482 // Helper code for comparisons
483 // ------------------------------------------------------------------------
485 var COMPARISON_FUNCTION_TABLE = {
486 // sort: and sort:- are allowed
487 '': compareSourceId_,
488 'active': compareActive_,
489 'desc': compareDescription_,
490 'description': compareDescription_,
491 'duration': compareDuration_,
492 'id': compareSourceId_,
493 'source': compareSourceType_,
494 'type': compareSourceType_
498 * Sorts active entries first. If both entries are inactive, puts the one
499 * that was active most recently first. If both are active, uses source ID,
500 * which puts longer lived events at the top, and behaves better than using
501 * duration or time of first event.
503 function compareActive_(source1, source2) {
504 if (!source1.isInactive() && source2.isInactive())
506 if (source1.isInactive() && !source2.isInactive())
508 if (source1.isInactive()) {
509 var deltaEndTime = source1.getEndTime() - source2.getEndTime();
510 if (deltaEndTime != 0) {
511 // The one that ended most recently (Highest end time) should be sorted
513 return -deltaEndTime;
515 // If both ended at the same time, then odds are they were related events,
516 // started one after another, so sort in the opposite order of their
517 // source IDs to get a more intuitive ordering.
518 return -compareSourceId_(source1, source2);
520 return compareSourceId_(source1, source2);
523 function compareDescription_(source1, source2) {
524 var source1Text = source1.getDescription().toLowerCase();
525 var source2Text = source2.getDescription().toLowerCase();
526 var compareResult = source1Text.localeCompare(source2Text);
527 if (compareResult != 0)
528 return compareResult;
529 return compareSourceId_(source1, source2);
532 function compareDuration_(source1, source2) {
533 var durationDifference = source2.getDuration() - source1.getDuration();
534 if (durationDifference)
535 return durationDifference;
536 return compareSourceId_(source1, source2);
540 * For the purposes of sorting by source IDs, entries without a source
541 * appear right after the SourceEntry with the highest source ID received
542 * before the sourceless entry. Any ambiguities are resolved by ordering
543 * the entries without a source by the order in which they were received.
545 function compareSourceId_(source1, source2) {
546 var sourceId1 = source1.getSourceId();
548 sourceId1 = source1.getMaxPreviousEntrySourceId();
549 var sourceId2 = source2.getSourceId();
551 sourceId2 = source2.getMaxPreviousEntrySourceId();
553 if (sourceId1 != sourceId2)
554 return sourceId1 - sourceId2;
556 // One or both have a negative ID. In either case, the source with the
557 // highest ID should be sorted first.
558 return source2.getSourceId() - source1.getSourceId();
561 function compareSourceType_(source1, source2) {
562 var source1Text = source1.getSourceTypeString();
563 var source2Text = source2.getSourceTypeString();
564 var compareResult = source1Text.localeCompare(source2Text);
565 if (compareResult != 0)
566 return compareResult;
567 return compareSourceId_(source1, source2);