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. 4 5/** 6 * EventsView displays a filtered list of all events sharing a source, and 7 * a details pane for the selected sources. 8 * 9 * +----------------------++----------------+ 10 * | filter box || | 11 * +----------------------+| | 12 * | || | 13 * | || | 14 * | || | 15 * | || | 16 * | source list || details | 17 * | || view | 18 * | || | 19 * | || | 20 * | || | 21 * | || | 22 * | || | 23 * | || | 24 * +----------------------++----------------+ 25 */ 26var EventsView = (function() { 27 'use strict'; 28 29 // How soon after updating the filter list the counter should be updated. 30 var REPAINT_FILTER_COUNTER_TIMEOUT_MS = 0; 31 32 // We inherit from View. 33 var superClass = View; 34 35 /* 36 * @constructor 37 */ 38 function EventsView() { 39 assertFirstConstructorCall(EventsView); 40 41 // Call superclass's constructor. 42 superClass.call(this); 43 44 // Initialize the sub-views. 45 var leftPane = new VerticalSplitView(new DivView(EventsView.TOPBAR_ID), 46 new DivView(EventsView.LIST_BOX_ID)); 47 48 this.detailsView_ = new DetailsView(EventsView.DETAILS_LOG_BOX_ID); 49 50 this.splitterView_ = new ResizableVerticalSplitView( 51 leftPane, this.detailsView_, new DivView(EventsView.SIZER_ID)); 52 53 SourceTracker.getInstance().addSourceEntryObserver(this); 54 55 this.tableBody_ = $(EventsView.TBODY_ID); 56 57 this.filterInput_ = $(EventsView.FILTER_INPUT_ID); 58 this.filterCount_ = $(EventsView.FILTER_COUNT_ID); 59 60 this.filterInput_.addEventListener('search', 61 this.onFilterTextChanged_.bind(this), true); 62 63 $(EventsView.SELECT_ALL_ID).addEventListener( 64 'click', this.selectAll_.bind(this), true); 65 66 $(EventsView.SORT_BY_ID_ID).addEventListener( 67 'click', this.sortById_.bind(this), true); 68 69 $(EventsView.SORT_BY_SOURCE_TYPE_ID).addEventListener( 70 'click', this.sortBySourceType_.bind(this), true); 71 72 $(EventsView.SORT_BY_DESCRIPTION_ID).addEventListener( 73 'click', this.sortByDescription_.bind(this), true); 74 75 new MouseOverHelp(EventsView.FILTER_HELP_ID, 76 EventsView.FILTER_HELP_HOVER_ID); 77 78 // Sets sort order and filter. 79 this.setFilter_(''); 80 81 this.initializeSourceList_(); 82 } 83 84 EventsView.TAB_ID = 'tab-handle-events'; 85 EventsView.TAB_NAME = 'Events'; 86 EventsView.TAB_HASH = '#events'; 87 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'; 102 103 cr.addSingletonGetter(EventsView); 104 105 EventsView.prototype = { 106 // Inherit the superclass's methods. 107 __proto__: superClass.prototype, 108 109 /** 110 * Initializes the list of source entries. If source entries are already, 111 * being displayed, removes them all in the process. 112 */ 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_(); 121 }, 122 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); 126 }, 127 128 show: function(isVisible) { 129 superClass.prototype.show.call(this, isVisible); 130 this.splitterView_.show(isVisible); 131 }, 132 133 getFilterText_: function() { 134 return this.filterInput_.value; 135 }, 136 137 setFilterText_: function(filterText) { 138 this.filterInput_.value = filterText; 139 this.onFilterTextChanged_(); 140 }, 141 142 onFilterTextChanged_: function() { 143 this.setFilter_(this.getFilterText_()); 144 }, 145 146 /** 147 * Updates text in the details view when privacy stripping is toggled. 148 */ 149 onPrivacyStrippingChanged: function() { 150 this.invalidateDetailsView_(); 151 }, 152 153 /** 154 * Updates text in the details view when time display mode is toggled. 155 */ 156 onUseRelativeTimesChanged: function() { 157 this.invalidateDetailsView_(); 158 }, 159 160 comparisonFuncWithReversing_: function(a, b) { 161 var result = this.comparisonFunction_(a, b); 162 if (this.doSortBackwards_) 163 result *= -1; 164 return result; 165 }, 166 167 sort_: function() { 168 var sourceEntries = []; 169 for (var id in this.sourceIdToRowMap_) { 170 sourceEntries.push(this.sourceIdToRowMap_[id].getSourceEntry()); 171 } 172 sourceEntries.sort(this.comparisonFuncWithReversing_.bind(this)); 173 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); 181 } 182 } 183 }, 184 185 setFilter_: function(filterText) { 186 var lastComparisonFunction = this.comparisonFunction_; 187 var lastDoSortBackwards = this.doSortBackwards_; 188 189 var filterParser = new SourceFilterParser(filterText); 190 this.currentFilter_ = filterParser.filter; 191 192 this.pickSortFunction_(filterParser.sort); 193 194 if (lastComparisonFunction != this.comparisonFunction_ || 195 lastDoSortBackwards != this.doSortBackwards_) { 196 this.sort_(); 197 } 198 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())); 203 } 204 }, 205 206 /** 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. 210 */ 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_; 217 } 218 }, 219 220 /** 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. 224 */ 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; 229 while (true) { 230 var prevSourceId = sourceRowAfter.getPreviousNodeSourceId(); 231 if (prevSourceId == null) 232 break; 233 var prevSourceRow = this.sourceIdToRowMap_[prevSourceId]; 234 if (this.comparisonFuncWithReversing_( 235 sourceRow.getSourceEntry(), 236 prevSourceRow.getSourceEntry()) >= 0) { 237 break; 238 } 239 sourceRowAfter = prevSourceRow; 240 } 241 if (sourceRowAfter != sourceRow) { 242 sourceRow.moveBefore(sourceRowAfter); 243 return; 244 } 245 246 var sourceRowBefore = sourceRow; 247 while (true) { 248 var nextSourceId = sourceRowBefore.getNextNodeSourceId(); 249 if (nextSourceId == null) 250 break; 251 var nextSourceRow = this.sourceIdToRowMap_[nextSourceId]; 252 if (this.comparisonFuncWithReversing_( 253 sourceRow.getSourceEntry(), 254 nextSourceRow.getSourceEntry()) <= 0) { 255 break; 256 } 257 sourceRowBefore = nextSourceRow; 258 } 259 if (sourceRowBefore != sourceRow) 260 sourceRow.moveAfter(sourceRowBefore); 261 }, 262 263 /** 264 * Called whenever SourceEntries are updated with new log entries. Updates 265 * the corresponding table rows, sort order, and the details view as needed. 266 */ 267 onSourceEntriesUpdated: function(sourceEntries) { 268 var isUpdatedSourceSelected = false; 269 var numNewSourceEntries = 0; 270 271 for (var i = 0; i < sourceEntries.length; ++i) { 272 var sourceEntry = sourceEntries[i]; 273 274 // Lookup the row. 275 var sourceRow = this.sourceIdToRowMap_[sourceEntry.getSourceId()]; 276 277 if (!sourceRow) { 278 sourceRow = new SourceRow(this, sourceEntry); 279 this.sourceIdToRowMap_[sourceEntry.getSourceId()] = sourceRow; 280 ++numNewSourceEntries; 281 } else { 282 sourceRow.onSourceUpdated(); 283 } 284 285 if (sourceRow.isSelected()) 286 isUpdatedSourceSelected = true; 287 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); 293 } 294 295 if (isUpdatedSourceSelected) 296 this.invalidateDetailsView_(); 297 if (numNewSourceEntries) 298 this.incrementPrefilterCount(numNewSourceEntries); 299 }, 300 301 /** 302 * Returns the SourceRow with the specified ID, if there is one. 303 * Otherwise, returns undefined. 304 */ 305 getSourceRow: function(id) { 306 return this.sourceIdToRowMap_[id]; 307 }, 308 309 /** 310 * Called whenever all log events are deleted. 311 */ 312 onAllSourceEntriesDeleted: function() { 313 this.initializeSourceList_(); 314 }, 315 316 /** 317 * Called when either a log file is loaded, after clearing the old entries, 318 * but before getting any new ones. 319 */ 320 onLoadLogStart: function() { 321 // Needed to sort new sourceless entries correctly. 322 this.maxReceivedSourceId_ = 0; 323 }, 324 325 onLoadLogFinish: function(data) { 326 return true; 327 }, 328 329 incrementPrefilterCount: function(offset) { 330 this.numPrefilter_ += offset; 331 this.invalidateFilterCounter_(); 332 }, 333 334 incrementPostfilterCount: function(offset) { 335 this.numPostfilter_ += offset; 336 this.invalidateFilterCounter_(); 337 }, 338 339 onSelectionChanged: function() { 340 this.invalidateDetailsView_(); 341 }, 342 343 clearSelection: function() { 344 var prevSelection = this.currentSelectedRows_; 345 this.currentSelectedRows_ = []; 346 347 // Unselect everything that is currently selected. 348 for (var i = 0; i < prevSelection.length; ++i) { 349 prevSelection[i].setSelected(false); 350 } 351 352 this.onSelectionChanged(); 353 }, 354 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); 360 } 361 } 362 event.preventDefault(); 363 }, 364 365 unselectAll_: function() { 366 var entries = this.currentSelectedRows_.slice(0); 367 for (var i = 0; i < entries.length; ++i) { 368 entries[i].setSelected(false); 369 } 370 }, 371 372 /** 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 375 * item. 376 */ 377 setParameters: function(params) { 378 if (params.q) { 379 this.unselectAll_(); 380 this.setFilterText_(params.q); 381 } 382 383 if (params.s) { 384 var sourceRow = this.sourceIdToRowMap_[params.s]; 385 if (sourceRow) { 386 sourceRow.setSelected(true); 387 this.scrollToSourceId(params.s); 388 } 389 } 390 }, 391 392 /** 393 * Scrolls to the source indicated by |sourceId|, if displayed. 394 */ 395 scrollToSourceId: function(sourceId) { 396 this.detailsView_.scrollToSourceId(sourceId); 397 }, 398 399 /** 400 * If already using the specified sort method, flips direction. Otherwise, 401 * removes pre-existing sort parameter before adding the new one. 402 */ 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; 407 408 filterText = 'sort:' + sortMethod + ' ' + filterText; 409 410 // If already using specified sortMethod, sort backwards. 411 if (!this.doSortBackwards_ && 412 COMPARISON_FUNCTION_TABLE[sortMethod] == this.comparisonFunction_) { 413 filterText = '-' + filterText; 414 } 415 416 this.setFilterText_(filterText.trim()); 417 }, 418 419 sortById_: function(event) { 420 this.toggleSortMethod_('id'); 421 }, 422 423 sortBySourceType_: function(event) { 424 this.toggleSortMethod_('source'); 425 }, 426 427 sortByDescription_: function(event) { 428 this.toggleSortMethod_('desc'); 429 }, 430 431 /** 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 435 * state changing. 436 */ 437 modifySelectionArray: function(sourceId, addToSelection) { 438 var sourceRow = this.sourceIdToRowMap_[sourceId]; 439 if (!sourceRow) 440 return; 441 // Find the index for |sourceEntry| in the current selection list. 442 var index = -1; 443 for (var i = 0; i < this.currentSelectedRows_.length; ++i) { 444 if (this.currentSelectedRows_[i] == sourceRow) { 445 index = i; 446 break; 447 } 448 } 449 450 if (index != -1 && !addToSelection) { 451 // Remove from the selection. 452 this.currentSelectedRows_.splice(index, 1); 453 } 454 455 if (index == -1 && addToSelection) { 456 this.currentSelectedRows_.push(sourceRow); 457 } 458 }, 459 460 getSelectedSourceEntries_: function() { 461 var sourceEntries = []; 462 for (var i = 0; i < this.currentSelectedRows_.length; ++i) { 463 sourceEntries.push(this.currentSelectedRows_[i].getSourceEntry()); 464 } 465 return sourceEntries; 466 }, 467 468 invalidateDetailsView_: function() { 469 this.detailsView_.setData(this.getSelectedSourceEntries_()); 470 }, 471 472 invalidateFilterCounter_: function() { 473 if (!this.outstandingRepaintFilterCounter_) { 474 this.outstandingRepaintFilterCounter_ = true; 475 window.setTimeout(this.repaintFilterCounter_.bind(this), 476 REPAINT_FILTER_COUNTER_TIMEOUT_MS); 477 } 478 }, 479 480 repaintFilterCounter_: function() { 481 this.outstandingRepaintFilterCounter_ = false; 482 this.filterCount_.innerHTML = ''; 483 addTextNode(this.filterCount_, 484 this.numPostfilter_ + ' of ' + this.numPrefilter_); 485 } 486 }; // end of prototype. 487 488 // ------------------------------------------------------------------------ 489 // Helper code for comparisons 490 // ------------------------------------------------------------------------ 491 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_ 502 }; 503 504 /** 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. 509 */ 510 function compareActive_(source1, source2) { 511 if (!source1.isInactive() && source2.isInactive()) 512 return -1; 513 if (source1.isInactive() && !source2.isInactive()) 514 return 1; 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 519 // first. 520 return -deltaEndTime; 521 } 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); 526 } 527 return compareSourceId_(source1, source2); 528 } 529 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); 537 } 538 539 function compareDuration_(source1, source2) { 540 var durationDifference = source2.getDuration() - source1.getDuration(); 541 if (durationDifference) 542 return durationDifference; 543 return compareSourceId_(source1, source2); 544 } 545 546 /** 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. 551 */ 552 function compareSourceId_(source1, source2) { 553 var sourceId1 = source1.getSourceId(); 554 if (sourceId1 < 0) 555 sourceId1 = source1.getMaxPreviousEntrySourceId(); 556 var sourceId2 = source2.getSourceId(); 557 if (sourceId2 < 0) 558 sourceId2 = source2.getMaxPreviousEntrySourceId(); 559 560 if (sourceId1 != sourceId2) 561 return sourceId1 - sourceId2; 562 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(); 566 } 567 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); 575 } 576 577 return EventsView; 578})(); 579 580