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_();
154 * Updates text in the details view when time display mode is toggled.
156 onUseRelativeTimesChanged: function() {
157 this.invalidateDetailsView_();
160 comparisonFuncWithReversing_: function(a
, b
) {
161 var result
= this.comparisonFunction_(a
, b
);
162 if (this.doSortBackwards_
)
168 var sourceEntries
= [];
169 for (var id
in this.sourceIdToRowMap_
) {
170 sourceEntries
.push(this.sourceIdToRowMap_
[id
].getSourceEntry());
172 sourceEntries
.sort(this.comparisonFuncWithReversing_
.bind(this));
174 // Reposition source rows from back to front.
175 for (var i
= sourceEntries
.length
- 2; i
>= 0; --i
) {
176 var sourceRow
= this.sourceIdToRowMap_
[sourceEntries
[i
].getSourceId()];
177 var nextSourceId
= sourceEntries
[i
+ 1].getSourceId();
178 if (sourceRow
.getNextNodeSourceId() != nextSourceId
) {
179 var nextSourceRow
= this.sourceIdToRowMap_
[nextSourceId
];
180 sourceRow
.moveBefore(nextSourceRow
);
185 setFilter_: function(filterText
) {
186 var lastComparisonFunction
= this.comparisonFunction_
;
187 var lastDoSortBackwards
= this.doSortBackwards_
;
189 var filterParser
= new SourceFilterParser(filterText
);
190 this.currentFilter_
= filterParser
.filter
;
192 this.pickSortFunction_(filterParser
.sort
);
194 if (lastComparisonFunction
!= this.comparisonFunction_
||
195 lastDoSortBackwards
!= this.doSortBackwards_
) {
199 // Iterate through all of the rows and see if they match the filter.
200 for (var id
in this.sourceIdToRowMap_
) {
201 var entry
= this.sourceIdToRowMap_
[id
];
202 entry
.setIsMatchedByFilter(this.currentFilter_(entry
.getSourceEntry()));
207 * Given a "sort" object with "method" and "backwards" keys, looks up and
208 * sets |comparisonFunction_| and |doSortBackwards_|. If the ID does not
209 * correspond to a sort function, defaults to sorting by ID.
211 pickSortFunction_: function(sort
) {
212 this.doSortBackwards_
= sort
.backwards
;
213 this.comparisonFunction_
= COMPARISON_FUNCTION_TABLE
[sort
.method
];
214 if (!this.comparisonFunction_
) {
215 this.doSortBackwards_
= false;
216 this.comparisonFunction_
= compareSourceId_
;
221 * Repositions |sourceRow|'s in the table using an insertion sort.
222 * Significantly faster than sorting the entire table again, when only
223 * one entry has changed.
225 insertionSort_: function(sourceRow
) {
226 // SourceRow that should be after |sourceRow|, if it needs
227 // to be moved earlier in the list.
228 var sourceRowAfter
= sourceRow
;
230 var prevSourceId
= sourceRowAfter
.getPreviousNodeSourceId();
231 if (prevSourceId
== null)
233 var prevSourceRow
= this.sourceIdToRowMap_
[prevSourceId
];
234 if (this.comparisonFuncWithReversing_(
235 sourceRow
.getSourceEntry(),
236 prevSourceRow
.getSourceEntry()) >= 0) {
239 sourceRowAfter
= prevSourceRow
;
241 if (sourceRowAfter
!= sourceRow
) {
242 sourceRow
.moveBefore(sourceRowAfter
);
246 var sourceRowBefore
= sourceRow
;
248 var nextSourceId
= sourceRowBefore
.getNextNodeSourceId();
249 if (nextSourceId
== null)
251 var nextSourceRow
= this.sourceIdToRowMap_
[nextSourceId
];
252 if (this.comparisonFuncWithReversing_(
253 sourceRow
.getSourceEntry(),
254 nextSourceRow
.getSourceEntry()) <= 0) {
257 sourceRowBefore
= nextSourceRow
;
259 if (sourceRowBefore
!= sourceRow
)
260 sourceRow
.moveAfter(sourceRowBefore
);
264 * Called whenever SourceEntries are updated with new log entries. Updates
265 * the corresponding table rows, sort order, and the details view as needed.
267 onSourceEntriesUpdated: function(sourceEntries
) {
268 var isUpdatedSourceSelected
= false;
269 var numNewSourceEntries
= 0;
271 for (var i
= 0; i
< sourceEntries
.length
; ++i
) {
272 var sourceEntry
= sourceEntries
[i
];
275 var sourceRow
= this.sourceIdToRowMap_
[sourceEntry
.getSourceId()];
278 sourceRow
= new SourceRow(this, sourceEntry
);
279 this.sourceIdToRowMap_
[sourceEntry
.getSourceId()] = sourceRow
;
280 ++numNewSourceEntries
;
282 sourceRow
.onSourceUpdated();
285 if (sourceRow
.isSelected())
286 isUpdatedSourceSelected
= true;
288 // TODO(mmenke): Fix sorting when sorting by duration.
289 // Duration continuously increases for all entries that
290 // are still active. This can result in incorrect
291 // sorting, until sort_ is called.
292 this.insertionSort_(sourceRow
);
295 if (isUpdatedSourceSelected
)
296 this.invalidateDetailsView_();
297 if (numNewSourceEntries
)
298 this.incrementPrefilterCount(numNewSourceEntries
);
302 * Returns the SourceRow with the specified ID, if there is one.
303 * Otherwise, returns undefined.
305 getSourceRow: function(id
) {
306 return this.sourceIdToRowMap_
[id
];
310 * Called whenever all log events are deleted.
312 onAllSourceEntriesDeleted: function() {
313 this.initializeSourceList_();
317 * Called when either a log file is loaded, after clearing the old entries,
318 * but before getting any new ones.
320 onLoadLogStart: function() {
321 // Needed to sort new sourceless entries correctly.
322 this.maxReceivedSourceId_
= 0;
325 onLoadLogFinish: function(data
) {
329 incrementPrefilterCount: function(offset
) {
330 this.numPrefilter_
+= offset
;
331 this.invalidateFilterCounter_();
334 incrementPostfilterCount: function(offset
) {
335 this.numPostfilter_
+= offset
;
336 this.invalidateFilterCounter_();
339 onSelectionChanged: function() {
340 this.invalidateDetailsView_();
343 clearSelection: function() {
344 var prevSelection
= this.currentSelectedRows_
;
345 this.currentSelectedRows_
= [];
347 // Unselect everything that is currently selected.
348 for (var i
= 0; i
< prevSelection
.length
; ++i
) {
349 prevSelection
[i
].setSelected(false);
352 this.onSelectionChanged();
355 selectAll_: function(event
) {
356 for (var id
in this.sourceIdToRowMap_
) {
357 var sourceRow
= this.sourceIdToRowMap_
[id
];
358 if (sourceRow
.isMatchedByFilter()) {
359 sourceRow
.setSelected(true);
362 event
.preventDefault();
365 unselectAll_: function() {
366 var entries
= this.currentSelectedRows_
.slice(0);
367 for (var i
= 0; i
< entries
.length
; ++i
) {
368 entries
[i
].setSelected(false);
373 * If |params| includes a query, replaces the current filter and unselects.
374 * all items. If it includes a selection, tries to select the relevant
377 setParameters: function(params
) {
380 this.setFilterText_(params
.q
);
384 var sourceRow
= this.sourceIdToRowMap_
[params
.s
];
386 sourceRow
.setSelected(true);
387 this.scrollToSourceId(params
.s
);
393 * Scrolls to the source indicated by |sourceId|, if displayed.
395 scrollToSourceId: function(sourceId
) {
396 this.detailsView_
.scrollToSourceId(sourceId
);
400 * If already using the specified sort method, flips direction. Otherwise,
401 * removes pre-existing sort parameter before adding the new one.
403 toggleSortMethod_: function(sortMethod
) {
404 // Get old filter text and remove old sort directives, if any.
405 var filterParser
= new SourceFilterParser(this.getFilterText_());
406 var filterText
= filterParser
.filterTextWithoutSort
;
408 filterText
= 'sort:' + sortMethod
+ ' ' + filterText
;
410 // If already using specified sortMethod, sort backwards.
411 if (!this.doSortBackwards_
&&
412 COMPARISON_FUNCTION_TABLE
[sortMethod
] == this.comparisonFunction_
) {
413 filterText
= '-' + filterText
;
416 this.setFilterText_(filterText
.trim());
419 sortById_: function(event
) {
420 this.toggleSortMethod_('id');
423 sortBySourceType_: function(event
) {
424 this.toggleSortMethod_('source');
427 sortByDescription_: function(event
) {
428 this.toggleSortMethod_('desc');
432 * Modifies the map of selected rows to include/exclude the one with
433 * |sourceId|, if present. Does not modify checkboxes or the LogView.
434 * Should only be called by a SourceRow in response to its selection
437 modifySelectionArray: function(sourceId
, addToSelection
) {
438 var sourceRow
= this.sourceIdToRowMap_
[sourceId
];
441 // Find the index for |sourceEntry| in the current selection list.
443 for (var i
= 0; i
< this.currentSelectedRows_
.length
; ++i
) {
444 if (this.currentSelectedRows_
[i
] == sourceRow
) {
450 if (index
!= -1 && !addToSelection
) {
451 // Remove from the selection.
452 this.currentSelectedRows_
.splice(index
, 1);
455 if (index
== -1 && addToSelection
) {
456 this.currentSelectedRows_
.push(sourceRow
);
460 getSelectedSourceEntries_: function() {
461 var sourceEntries
= [];
462 for (var i
= 0; i
< this.currentSelectedRows_
.length
; ++i
) {
463 sourceEntries
.push(this.currentSelectedRows_
[i
].getSourceEntry());
465 return sourceEntries
;
468 invalidateDetailsView_: function() {
469 this.detailsView_
.setData(this.getSelectedSourceEntries_());
472 invalidateFilterCounter_: function() {
473 if (!this.outstandingRepaintFilterCounter_
) {
474 this.outstandingRepaintFilterCounter_
= true;
475 window
.setTimeout(this.repaintFilterCounter_
.bind(this),
476 REPAINT_FILTER_COUNTER_TIMEOUT_MS
);
480 repaintFilterCounter_: function() {
481 this.outstandingRepaintFilterCounter_
= false;
482 this.filterCount_
.innerHTML
= '';
483 addTextNode(this.filterCount_
,
484 this.numPostfilter_
+ ' of ' + this.numPrefilter_
);
486 }; // end of prototype.
488 // ------------------------------------------------------------------------
489 // Helper code for comparisons
490 // ------------------------------------------------------------------------
492 var COMPARISON_FUNCTION_TABLE
= {
493 // sort: and sort:- are allowed
494 '': compareSourceId_
,
495 'active': compareActive_
,
496 'desc': compareDescription_
,
497 'description': compareDescription_
,
498 'duration': compareDuration_
,
499 'id': compareSourceId_
,
500 'source': compareSourceType_
,
501 'type': compareSourceType_
505 * Sorts active entries first. If both entries are inactive, puts the one
506 * that was active most recently first. If both are active, uses source ID,
507 * which puts longer lived events at the top, and behaves better than using
508 * duration or time of first event.
510 function compareActive_(source1
, source2
) {
511 if (!source1
.isInactive() && source2
.isInactive())
513 if (source1
.isInactive() && !source2
.isInactive())
515 if (source1
.isInactive()) {
516 var deltaEndTime
= source1
.getEndTicks() - source2
.getEndTicks();
517 if (deltaEndTime
!= 0) {
518 // The one that ended most recently (Highest end time) should be sorted
520 return -deltaEndTime
;
522 // If both ended at the same time, then odds are they were related events,
523 // started one after another, so sort in the opposite order of their
524 // source IDs to get a more intuitive ordering.
525 return -compareSourceId_(source1
, source2
);
527 return compareSourceId_(source1
, source2
);
530 function compareDescription_(source1
, source2
) {
531 var source1Text
= source1
.getDescription().toLowerCase();
532 var source2Text
= source2
.getDescription().toLowerCase();
533 var compareResult
= source1Text
.localeCompare(source2Text
);
534 if (compareResult
!= 0)
535 return compareResult
;
536 return compareSourceId_(source1
, source2
);
539 function compareDuration_(source1
, source2
) {
540 var durationDifference
= source2
.getDuration() - source1
.getDuration();
541 if (durationDifference
)
542 return durationDifference
;
543 return compareSourceId_(source1
, source2
);
547 * For the purposes of sorting by source IDs, entries without a source
548 * appear right after the SourceEntry with the highest source ID received
549 * before the sourceless entry. Any ambiguities are resolved by ordering
550 * the entries without a source by the order in which they were received.
552 function compareSourceId_(source1
, source2
) {
553 var sourceId1
= source1
.getSourceId();
555 sourceId1
= source1
.getMaxPreviousEntrySourceId();
556 var sourceId2
= source2
.getSourceId();
558 sourceId2
= source2
.getMaxPreviousEntrySourceId();
560 if (sourceId1
!= sourceId2
)
561 return sourceId1
- sourceId2
;
563 // One or both have a negative ID. In either case, the source with the
564 // highest ID should be sorted first.
565 return source2
.getSourceId() - source1
.getSourceId();
568 function compareSourceType_(source1
, source2
) {
569 var source1Text
= source1
.getSourceTypeString();
570 var source2Text
= source2
.getSourceTypeString();
571 var compareResult
= source1Text
.localeCompare(source2Text
);
572 if (compareResult
!= 0)
573 return compareResult
;
574 return compareSourceId_(source1
, source2
);