OLD | NEW |
(Empty) | |
| 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 */ |
| 26 var 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 |
OLD | NEW |