1<!DOCTYPE html> 2<!-- 3Copyright (c) 2012 The Chromium Authors. All rights reserved. 4Use of this source code is governed by a BSD-style license that can be 5found in the LICENSE file. 6--> 7 8<link rel="import" href="/tracing/base/event.html"> 9<link rel="import" href="/tracing/base/settings.html"> 10<link rel="import" href="/tracing/base/task.html"> 11<link rel="import" href="/tracing/core/filter.html"> 12<link rel="import" href="/tracing/model/event.html"> 13<link rel="import" href="/tracing/model/event_set.html"> 14<link rel="import" href="/tracing/model/x_marker_annotation.html"> 15<link rel="import" href="/tracing/ui/base/hotkey_controller.html"> 16<link rel="import" href="/tracing/ui/base/mouse_mode_selector.html"> 17<link rel="import" href="/tracing/ui/base/timing_tool.html"> 18<link rel="import" href="/tracing/ui/base/ui.html"> 19<link rel="import" href="/tracing/ui/timeline_display_transform_animations.html"> 20<link rel="import" href="/tracing/ui/timeline_viewport.html"> 21<link rel="import" href="/tracing/ui/tracks/drawing_container.html"> 22<link rel="import" href="/tracing/ui/tracks/model_track.html"> 23<link rel="import" href="/tracing/ui/tracks/ruler_track.html"> 24<link rel="import" href="/tracing/value/unit.html"> 25 26<!-- 27 Interactive visualizaiton of Model objects based loosely on gantt charts. 28 Each thread in the Model is given a set of Tracks, one per subrow in the 29 thread. The TimelineTrackView class acts as a controller, creating the 30 individual tracks, while Tracks do actual drawing. 31 32 Visually, the TimelineTrackView produces (prettier) visualizations like the 33 following: 34 Thread1: AAAAAAAAAA AAAAA 35 BBBB BB 36 Thread2: CCCCCC CCCCC 37--> 38<polymer-element name='tr-ui-timeline-track-view'> 39 <template> 40 <style> 41 :host { 42 -webkit-box-orient: vertical; 43 display: -webkit-box; 44 position: relative; 45 } 46 47 :host ::content * { 48 -webkit-user-select: none; 49 cursor: default; 50 } 51 52 #drag_box { 53 background-color: rgba(0, 0, 255, 0.25); 54 border: 1px solid rgb(0, 0, 96); 55 font-size: 75%; 56 position: fixed; 57 } 58 59 #hint_text { 60 position: absolute; 61 bottom: 6px; 62 right: 6px; 63 font-size: 8pt; 64 } 65 </style> 66 <content></content> 67 68 <div id='drag_box'></div> 69 <div id='hint_text'></div> 70 71 <tv-ui-b-hotkey-controller id='hotkey_controller'> 72 </tv-ui-b-hotkey-controller> 73 </template> 74 75 <script> 76 'use strict'; 77 78 Polymer({ 79 ready: function() { 80 this.displayTransform_ = new tr.ui.TimelineDisplayTransform(); 81 this.model_ = undefined; 82 83 this.timelineView_ = undefined; 84 85 this.viewport_ = new tr.ui.TimelineViewport(this); 86 this.viewportDisplayTransformAtMouseDown_ = undefined; 87 this.brushingStateController_ = undefined; 88 89 this.rulerTrackContainer_ = 90 new tr.ui.tracks.DrawingContainer(this.viewport_); 91 this.appendChild(this.rulerTrackContainer_); 92 this.rulerTrackContainer_.invalidate(); 93 94 this.rulerTrack_ = new tr.ui.tracks.RulerTrack(this.viewport_); 95 this.rulerTrackContainer_.appendChild(this.rulerTrack_); 96 97 this.upperModelTrack_ = new tr.ui.tracks.ModelTrack(this.viewport_); 98 this.upperModelTrack_.upperMode = true; 99 this.rulerTrackContainer_.appendChild(this.upperModelTrack_); 100 101 this.modelTrackContainer_ = 102 new tr.ui.tracks.DrawingContainer(this.viewport_); 103 this.appendChild(this.modelTrackContainer_); 104 this.modelTrackContainer_.style.display = 'block'; 105 this.modelTrackContainer_.invalidate(); 106 107 this.viewport_.modelTrackContainer = this.modelTrackContainer_; 108 109 this.modelTrack_ = new tr.ui.tracks.ModelTrack(this.viewport_); 110 this.modelTrackContainer_.appendChild(this.modelTrack_); 111 112 this.timingTool_ = new tr.ui.b.TimingTool(this.viewport_, this); 113 114 this.initMouseModeSelector(); 115 116 this.hideDragBox_(); 117 118 this.initHintText_(); 119 120 this.onSelectionChanged_ = this.onSelectionChanged_.bind(this); 121 122 this.onDblClick_ = this.onDblClick_.bind(this); 123 this.addEventListener('dblclick', this.onDblClick_); 124 125 this.onMouseWheel_ = this.onMouseWheel_.bind(this); 126 this.addEventListener('mousewheel', this.onMouseWheel_); 127 128 this.onMouseDown_ = this.onMouseDown_.bind(this); 129 this.addEventListener('mousedown', this.onMouseDown_); 130 131 this.onMouseMove_ = this.onMouseMove_.bind(this); 132 this.addEventListener('mousemove', this.onMouseMove_); 133 134 this.onTouchStart_ = this.onTouchStart_.bind(this); 135 this.addEventListener('touchstart', this.onTouchStart_); 136 137 this.onTouchMove_ = this.onTouchMove_.bind(this); 138 this.addEventListener('touchmove', this.onTouchMove_); 139 140 this.onTouchEnd_ = this.onTouchEnd_.bind(this); 141 this.addEventListener('touchend', this.onTouchEnd_); 142 143 144 this.addHotKeys_(); 145 146 this.mouseViewPosAtMouseDown_ = {x: 0, y: 0}; 147 this.lastMouseViewPos_ = {x: 0, y: 0}; 148 149 this.lastTouchViewPositions_ = []; 150 151 this.alert_ = undefined; 152 153 this.isPanningAndScanning_ = false; 154 this.isZooming_ = false; 155 }, 156 157 initMouseModeSelector: function() { 158 this.mouseModeSelector_ = document.createElement( 159 'tr-ui-b-mouse-mode-selector'); 160 this.mouseModeSelector_.targetElement = this; 161 this.appendChild(this.mouseModeSelector_); 162 163 this.mouseModeSelector_.addEventListener('beginpan', 164 this.onBeginPanScan_.bind(this)); 165 this.mouseModeSelector_.addEventListener('updatepan', 166 this.onUpdatePanScan_.bind(this)); 167 this.mouseModeSelector_.addEventListener('endpan', 168 this.onEndPanScan_.bind(this)); 169 170 this.mouseModeSelector_.addEventListener('beginselection', 171 this.onBeginSelection_.bind(this)); 172 this.mouseModeSelector_.addEventListener('updateselection', 173 this.onUpdateSelection_.bind(this)); 174 this.mouseModeSelector_.addEventListener('endselection', 175 this.onEndSelection_.bind(this)); 176 177 this.mouseModeSelector_.addEventListener('beginzoom', 178 this.onBeginZoom_.bind(this)); 179 this.mouseModeSelector_.addEventListener('updatezoom', 180 this.onUpdateZoom_.bind(this)); 181 this.mouseModeSelector_.addEventListener('endzoom', 182 this.onEndZoom_.bind(this)); 183 184 this.mouseModeSelector_.addEventListener('entertiming', 185 this.timingTool_.onEnterTiming.bind(this.timingTool_)); 186 this.mouseModeSelector_.addEventListener('begintiming', 187 this.timingTool_.onBeginTiming.bind(this.timingTool_)); 188 this.mouseModeSelector_.addEventListener('updatetiming', 189 this.timingTool_.onUpdateTiming.bind(this.timingTool_)); 190 this.mouseModeSelector_.addEventListener('endtiming', 191 this.timingTool_.onEndTiming.bind(this.timingTool_)); 192 this.mouseModeSelector_.addEventListener('exittiming', 193 this.timingTool_.onExitTiming.bind(this.timingTool_)); 194 195 var m = tr.ui.b.MOUSE_SELECTOR_MODE; 196 this.mouseModeSelector_.supportedModeMask = 197 m.SELECTION | m.PANSCAN | m.ZOOM | m.TIMING; 198 this.mouseModeSelector_.settingsKey = 199 'timelineTrackView.mouseModeSelector'; 200 this.mouseModeSelector_.setKeyCodeForMode(m.PANSCAN, '2'.charCodeAt(0)); 201 this.mouseModeSelector_.setKeyCodeForMode(m.SELECTION, '1'.charCodeAt(0)); 202 this.mouseModeSelector_.setKeyCodeForMode(m.ZOOM, '3'.charCodeAt(0)); 203 this.mouseModeSelector_.setKeyCodeForMode(m.TIMING, '4'.charCodeAt(0)); 204 205 this.mouseModeSelector_.setModifierForAlternateMode( 206 m.SELECTION, tr.ui.b.MODIFIER.SHIFT); 207 this.mouseModeSelector_.setModifierForAlternateMode( 208 m.PANSCAN, tr.ui.b.MODIFIER.SPACE); 209 }, 210 211 get brushingStateController() { 212 return this.brushingStateController_; 213 }, 214 215 set brushingStateController(brushingStateController) { 216 if (this.brushingStateController_) { 217 this.brushingStateController_.removeEventListener('change', 218 this.onSelectionChanged_); 219 } 220 this.brushingStateController_ = brushingStateController; 221 if (this.brushingStateController_) { 222 this.brushingStateController_.addEventListener('change', 223 this.onSelectionChanged_); 224 } 225 }, 226 227 set timelineView(view) { 228 this.timelineView_ = view; 229 }, 230 231 onSelectionChanged_: function() { 232 this.showHintText_('Press \'m\' to mark current selection'); 233 this.viewport_.dispatchChangeEvent(); 234 }, 235 236 set selection(selection) { 237 throw new Error('DO NOT CALL THIS'); 238 }, 239 240 set highlight(highlight) { 241 throw new Error('DO NOT CALL THIS'); 242 }, 243 244 detach: function() { 245 this.modelTrack_.detach(); 246 this.upperModelTrack_.detach(); 247 248 this.viewport_.detach(); 249 }, 250 251 get viewport() { 252 return this.viewport_; 253 }, 254 255 get model() { 256 return this.model_; 257 }, 258 259 set model(model) { 260 if (!model) 261 throw new Error('Model cannot be undefined'); 262 263 var modelInstanceChanged = this.model_ !== model; 264 this.model_ = model; 265 this.modelTrack_.model = model; 266 this.upperModelTrack_.model = model; 267 268 // Set up a reasonable viewport. 269 if (modelInstanceChanged) 270 this.viewport_.setWhenPossible(this.setInitialViewport_.bind(this)); 271 }, 272 273 get hasVisibleContent() { 274 return this.modelTrack_.hasVisibleContent || 275 this.upperModelTrack_.hasVisibleContent; 276 }, 277 278 setInitialViewport_: function() { 279 // We need the canvas size to be up-to-date at this point. We maybe in 280 // here before the raf fires, so the size may have not been updated since 281 // the canvas was resized. 282 this.modelTrackContainer_.updateCanvasSizeIfNeeded_(); 283 var w = this.modelTrackContainer_.canvas.width; 284 285 var min; 286 var range; 287 288 if (this.model_.bounds.isEmpty) { 289 min = 0; 290 range = 1000; 291 } else if (this.model_.bounds.range === 0) { 292 min = this.model_.bounds.min; 293 range = 1000; 294 } else { 295 min = this.model_.bounds.min; 296 range = this.model_.bounds.range; 297 } 298 299 var boost = range * 0.15; 300 this.displayTransform_.set(this.viewport_.currentDisplayTransform); 301 this.displayTransform_.xSetWorldBounds( 302 min - boost, min + range + boost, w); 303 this.viewport_.setDisplayTransformImmediately(this.displayTransform_); 304 }, 305 306 /** 307 * @param {Filter} filter The filter to use for finding matches. 308 * @param {Selection} selection The selection to add matches to. 309 * @return {Task} which performs the filtering. 310 */ 311 addAllEventsMatchingFilterToSelectionAsTask: function(filter, selection) { 312 var modelTrack = this.modelTrack_; 313 var firstT = modelTrack.addAllEventsMatchingFilterToSelectionAsTask( 314 filter, selection); 315 var lastT = firstT.after(function() { 316 this.upperModelTrack_.addAllEventsMatchingFilterToSelection( 317 filter, selection); 318 319 }, this); 320 return firstT; 321 }, 322 323 onMouseMove_: function(e) { 324 // Zooming requires the delta since the last mousemove so we need to avoid 325 // tracking it when the zoom interaction is active. 326 if (this.isZooming_) 327 return; 328 329 this.storeLastMousePos_(e); 330 }, 331 332 onTouchStart_: function(e) { 333 this.storeLastTouchPositions_(e); 334 this.focusElements_(); 335 }, 336 337 onTouchMove_: function(e) { 338 e.preventDefault(); 339 this.onUpdateTransformForTouch_(e); 340 }, 341 342 onTouchEnd_: function(e) { 343 this.storeLastTouchPositions_(e); 344 this.focusElements_(); 345 }, 346 347 addHotKeys_: function() { 348 this.addKeyDownHotKeys_(); 349 this.addKeyPressHotKeys_(); 350 }, 351 352 addKeyPressHotKeys_: function() { 353 var addBinding = function(dict) { 354 dict.eventType = 'keypress'; 355 dict.useCapture = false; 356 dict.thisArg = this; 357 var binding = new tr.ui.b.HotKey(dict); 358 this.$.hotkey_controller.addHotKey(binding); 359 }.bind(this); 360 361 addBinding({ 362 keyCodes: ['w'.charCodeAt(0), ','.charCodeAt(0)], 363 callback: function(e) { 364 this.zoomBy_(1.5, true); 365 e.stopPropagation(); 366 } 367 }); 368 369 addBinding({ 370 keyCodes: ['s'.charCodeAt(0), 'o'.charCodeAt(0)], 371 callback: function(e) { 372 this.zoomBy_(1 / 1.5, true); 373 e.stopPropagation(); 374 } 375 }); 376 377 addBinding({ 378 keyCode: 'g'.charCodeAt(0), 379 callback: function(e) { 380 this.onGridToggle_(true); 381 e.stopPropagation(); 382 } 383 }); 384 385 addBinding({ 386 keyCode: 'G'.charCodeAt(0), 387 callback: function(e) { 388 this.onGridToggle_(false); 389 e.stopPropagation(); 390 } 391 }); 392 393 addBinding({ 394 keyCodes: ['W'.charCodeAt(0), '<'.charCodeAt(0)], 395 callback: function(e) { 396 this.zoomBy_(10, true); 397 e.stopPropagation(); 398 } 399 }); 400 401 addBinding({ 402 keyCodes: ['S'.charCodeAt(0), 'O'.charCodeAt(0)], 403 callback: function(e) { 404 this.zoomBy_(1 / 10, true); 405 e.stopPropagation(); 406 } 407 }); 408 409 addBinding({ 410 keyCode: 'a'.charCodeAt(0), 411 callback: function(e) { 412 this.queueSmoothPan_(this.viewWidth_ * 0.3, 0); 413 e.stopPropagation(); 414 } 415 }); 416 417 addBinding({ 418 keyCodes: ['d'.charCodeAt(0), 'e'.charCodeAt(0)], 419 callback: function(e) { 420 this.queueSmoothPan_(this.viewWidth_ * -0.3, 0); 421 e.stopPropagation(); 422 } 423 }); 424 425 addBinding({ 426 keyCode: 'A'.charCodeAt(0), 427 callback: function(e) { 428 this.queueSmoothPan_(viewWidth * 0.5, 0); 429 e.stopPropagation(); 430 } 431 }); 432 433 addBinding({ 434 keyCode: 'D'.charCodeAt(0), 435 callback: function(e) { 436 this.queueSmoothPan_(viewWidth * -0.5, 0); 437 e.stopPropagation(); 438 } 439 }); 440 441 addBinding({ 442 keyCode: '0'.charCodeAt(0), 443 callback: function(e) { 444 this.setInitialViewport_(); 445 e.stopPropagation(); 446 } 447 }); 448 449 addBinding({ 450 keyCode: 'f'.charCodeAt(0), 451 callback: function(e) { 452 this.zoomToSelection(); 453 e.stopPropagation(); 454 } 455 }); 456 457 addBinding({ 458 keyCode: 'm'.charCodeAt(0), 459 callback: function(e) { 460 this.setCurrentSelectionAsInterestRange_(); 461 e.stopPropagation(); 462 } 463 }); 464 465 addBinding({ 466 keyCode: 'h'.charCodeAt(0), 467 callback: function(e) { 468 this.toggleHighDetails_(); 469 e.stopPropagation(); 470 } 471 }); 472 }, 473 474 get viewWidth_() { 475 return this.modelTrackContainer_.canvas.clientWidth; 476 }, 477 478 addKeyDownHotKeys_: function() { 479 var addBinding = function(dict) { 480 dict.eventType = 'keydown'; 481 dict.useCapture = false; 482 dict.thisArg = this; 483 var binding = new tr.ui.b.HotKey(dict); 484 this.$.hotkey_controller.addHotKey(binding); 485 }.bind(this); 486 487 addBinding({ 488 keyCode: 37, // Left arrow. 489 callback: function(e) { 490 var curSel = this.brushingStateController_.selection; 491 var sel = this.viewport.getShiftedSelection(curSel, -1); 492 493 if (sel) { 494 this.brushingStateController.changeSelectionFromTimeline(sel); 495 this.panToSelection(); 496 } else { 497 this.queueSmoothPan_(this.viewWidth_ * 0.3, 0); 498 } 499 e.preventDefault(); 500 e.stopPropagation(); 501 } 502 }); 503 504 addBinding({ 505 keyCode: 39, // Right arrow. 506 callback: function(e) { 507 var curSel = this.brushingStateController_.selection; 508 var sel = this.viewport.getShiftedSelection(curSel, 1); 509 if (sel) { 510 this.brushingStateController.changeSelectionFromTimeline(sel); 511 this.panToSelection(); 512 } else { 513 this.queueSmoothPan_(-this.viewWidth_ * 0.3, 0); 514 } 515 e.preventDefault(); 516 e.stopPropagation(); 517 } 518 }); 519 }, 520 521 onDblClick_: function(e) { 522 if (this.mouseModeSelector_.mode !== 523 tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION) 524 return; 525 526 var curSelection = this.brushingStateController_.selection; 527 if (!curSelection.length || !curSelection[0].title) 528 return; 529 530 var selection = new tr.model.EventSet(); 531 var filter = new tr.c.ExactTitleFilter(curSelection[0].title); 532 this.modelTrack_.addAllEventsMatchingFilterToSelection(filter, 533 selection); 534 535 this.brushingStateController.changeSelectionFromTimeline(selection); 536 }, 537 538 onMouseWheel_: function(e) { 539 if (!e.altKey) 540 return; 541 542 var delta = e.wheelDelta / 120; 543 var zoomScale = Math.pow(1.5, delta); 544 this.zoomBy_(zoomScale); 545 e.preventDefault(); 546 }, 547 548 onMouseDown_: function(e) { 549 if (this.mouseModeSelector_.mode !== 550 tr.ui.b.MOUSE_SELECTOR_MODE.SELECTION) 551 return; 552 553 // Mouse down must start on ruler track for crosshair guide lines to draw. 554 if (e.target !== this.rulerTrack_) 555 return; 556 557 // Make sure we don't start a selection drag event here. 558 this.dragBeginEvent_ = undefined; 559 560 // Remove nav string marker if it exists, since we're clearing the 561 // find control box. 562 if (this.xNavStringMarker_) { 563 this.model.removeAnnotation(this.xNavStringMarker_); 564 this.xNavStringMarker_ = undefined; 565 } 566 567 var dt = this.viewport_.currentDisplayTransform; 568 tr.ui.b.trackMouseMovesUntilMouseUp(function(e) { // Mouse move handler. 569 // If mouse event is on ruler, don't do anything. 570 if (e.target === this.rulerTrack_) 571 return; 572 573 var relativePosition = this.extractRelativeMousePosition_(e); 574 var loc = tr.model.Location.fromViewCoordinates( 575 this.viewport_, relativePosition.x, relativePosition.y); 576 // Not all points on the timeline represents a valid location. 577 // ex. process header tracks, letter dot tracks. 578 if (!loc) 579 return; 580 581 if (this.guideLineAnnotation_ === undefined) { 582 this.guideLineAnnotation_ = 583 new tr.model.XMarkerAnnotation(loc.xWorld); 584 this.model.addAnnotation(this.guideLineAnnotation_); 585 } else { 586 this.guideLineAnnotation_.timestamp = loc.xWorld; 587 this.modelTrackContainer_.invalidate(); 588 } 589 590 // Set the findcontrol's text to nav string of current state. 591 var state = new tr.ui.b.UIState(loc, 592 this.viewport_.currentDisplayTransform.scaleX); 593 this.timelineView_.setFindCtlText( 594 state.toUserFriendlyString(this.viewport_)); 595 }.bind(this), 596 undefined, // Mouse up handler. 597 function onKeyUpDuringDrag() { 598 if (this.dragBeginEvent_) { 599 this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_, 600 this.dragBoxXEnd_, this.dragBoxYEnd_); 601 } 602 }.bind(this)); 603 }, 604 605 queueSmoothPan_: function(viewDeltaX, deltaY) { 606 var deltaX = this.viewport_.currentDisplayTransform.xViewVectorToWorld( 607 viewDeltaX); 608 var animation = new tr.ui.TimelineDisplayTransformPanAnimation( 609 deltaX, deltaY); 610 this.viewport_.queueDisplayTransformAnimation(animation); 611 }, 612 613 /** 614 * Zoom in or out on the timeline by the given scale factor. 615 * @param {Number} scale The scale factor to apply. If <1, zooms out. 616 * @param {boolean} Whether to change the zoom level smoothly. 617 */ 618 zoomBy_: function(scale, smooth) { 619 if (scale <= 0) { 620 return; 621 } 622 623 smooth = !!smooth; 624 var vp = this.viewport_; 625 var pixelRatio = window.devicePixelRatio || 1; 626 627 var goalFocalPointXView = this.lastMouseViewPos_.x * pixelRatio; 628 var goalFocalPointXWorld = vp.currentDisplayTransform.xViewToWorld( 629 goalFocalPointXView); 630 if (smooth) { 631 var animation = new tr.ui.TimelineDisplayTransformZoomToAnimation( 632 goalFocalPointXWorld, goalFocalPointXView, 633 vp.currentDisplayTransform.panY, 634 scale); 635 vp.queueDisplayTransformAnimation(animation); 636 } else { 637 this.displayTransform_.set(vp.currentDisplayTransform); 638 this.displayTransform_.scaleX *= scale; 639 this.displayTransform_.xPanWorldPosToViewPos( 640 goalFocalPointXWorld, goalFocalPointXView, this.viewWidth_); 641 vp.setDisplayTransformImmediately(this.displayTransform_); 642 } 643 }, 644 645 /** 646 * Zoom into the current selection. 647 */ 648 zoomToSelection: function() { 649 if (!this.brushingStateController.selectionOfInterest.length) 650 return; 651 652 var bounds = this.brushingStateController.selectionOfInterest.bounds; 653 if (!bounds.range) 654 return; 655 656 var worldCenter = bounds.center; 657 var viewCenter = this.modelTrackContainer_.canvas.width / 2; 658 var adjustedWorldRange = bounds.range * 1.25; 659 var newScale = this.modelTrackContainer_.canvas.width / 660 adjustedWorldRange; 661 var zoomInRatio = newScale / 662 this.viewport_.currentDisplayTransform.scaleX; 663 664 var animation = new tr.ui.TimelineDisplayTransformZoomToAnimation( 665 worldCenter, viewCenter, 666 this.viewport_.currentDisplayTransform.panY, 667 zoomInRatio); 668 this.viewport_.queueDisplayTransformAnimation(animation); 669 }, 670 671 /** 672 * Pan the view so the current selection becomes visible. 673 */ 674 panToSelection: function() { 675 if (!this.brushingStateController.selectionOfInterest.length) 676 return; 677 678 var bounds = this.brushingStateController.selectionOfInterest.bounds; 679 var worldCenter = bounds.center; 680 var viewWidth = this.viewWidth_; 681 682 var dt = this.viewport_.currentDisplayTransform; 683 if (false && !bounds.range) { 684 if (dt.xWorldToView(bounds.center) < 0 || 685 dt.xWorldToView(bounds.center) > viewWidth) { 686 this.displayTransform_.set(dt); 687 this.displayTransform_.xPanWorldPosToViewPos( 688 worldCenter, 'center', viewWidth); 689 var deltaX = this.displayTransform_.panX - dt.panX; 690 var animation = new tr.ui.TimelineDisplayTransformPanAnimation( 691 deltaX, 0); 692 this.viewport_.queueDisplayTransformAnimation(animation); 693 } 694 return; 695 } 696 697 this.displayTransform_.set(dt); 698 this.displayTransform_.xPanWorldBoundsIntoView( 699 bounds.min, 700 bounds.max, 701 viewWidth); 702 var deltaX = this.displayTransform_.panX - dt.panX; 703 var animation = new tr.ui.TimelineDisplayTransformPanAnimation( 704 deltaX, 0); 705 this.viewport_.queueDisplayTransformAnimation(animation); 706 }, 707 708 navToPosition: function(uiState, showNavLine) { 709 var location = uiState.location; 710 var scaleX = uiState.scaleX; 711 var track = location.getContainingTrack(this.viewport_); 712 713 var worldCenter = location.xWorld; 714 var viewCenter = this.modelTrackContainer_.canvas.width / 5; 715 var zoomInRatio = scaleX / 716 this.viewport_.currentDisplayTransform.scaleX; 717 718 // Vertically scroll so track is in view. 719 track.scrollIntoViewIfNeeded(); 720 721 // Perform zoom and panX animation. 722 var animation = new tr.ui.TimelineDisplayTransformZoomToAnimation( 723 worldCenter, viewCenter, 724 this.viewport_.currentDisplayTransform.panY, 725 zoomInRatio); 726 this.viewport_.queueDisplayTransformAnimation(animation); 727 728 if (!showNavLine) 729 return; 730 // Add an X Marker Annotation at the specified timestamp. 731 if (this.xNavStringMarker_) 732 this.model.removeAnnotation(this.xNavStringMarker_); 733 this.xNavStringMarker_ = 734 new tr.model.XMarkerAnnotation(worldCenter); 735 this.model.addAnnotation(this.xNavStringMarker_); 736 }, 737 738 setCurrentSelectionAsInterestRange_: function() { 739 var selectionBounds = this.brushingStateController_.selection.bounds; 740 if (selectionBounds.empty) { 741 this.viewport_.interestRange.reset(); 742 return; 743 } 744 745 if (this.viewport_.interestRange.min == selectionBounds.min && 746 this.viewport_.interestRange.max == selectionBounds.max) 747 this.viewport_.interestRange.reset(); 748 else 749 this.viewport_.interestRange.set(selectionBounds); 750 }, 751 752 toggleHighDetails_: function() { 753 this.viewport_.highDetails = !this.viewport_.highDetails; 754 }, 755 756 hideDragBox_: function() { 757 this.$.drag_box.style.left = '-1000px'; 758 this.$.drag_box.style.top = '-1000px'; 759 this.$.drag_box.style.width = 0; 760 this.$.drag_box.style.height = 0; 761 }, 762 763 setDragBoxPosition_: function(xStart, yStart, xEnd, yEnd) { 764 var loY = Math.min(yStart, yEnd); 765 var hiY = Math.max(yStart, yEnd); 766 var loX = Math.min(xStart, xEnd); 767 var hiX = Math.max(xStart, xEnd); 768 var modelTrackRect = this.modelTrack_.getBoundingClientRect(); 769 var dragRect = {left: loX, top: loY, width: hiX - loX, height: hiY - loY}; 770 771 dragRect.right = dragRect.left + dragRect.width; 772 dragRect.bottom = dragRect.top + dragRect.height; 773 774 var modelTrackContainerRect = 775 this.modelTrackContainer_.getBoundingClientRect(); 776 var clipRect = { 777 left: modelTrackContainerRect.left, 778 top: modelTrackContainerRect.top, 779 right: modelTrackContainerRect.right, 780 bottom: modelTrackContainerRect.bottom 781 }; 782 783 var headingWidth = window.getComputedStyle( 784 this.querySelector('tr-ui-heading')).width; 785 var trackTitleWidth = parseInt(headingWidth); 786 clipRect.left = clipRect.left + trackTitleWidth; 787 788 var intersectRect_ = function(r1, r2) { 789 if (r2.left > r1.right || r2.right < r1.left || 790 r2.top > r1.bottom || r2.bottom < r1.top) 791 return false; 792 793 var results = {}; 794 results.left = Math.max(r1.left, r2.left); 795 results.top = Math.max(r1.top, r2.top); 796 results.right = Math.min(r1.right, r2.right); 797 results.bottom = Math.min(r1.bottom, r2.bottom); 798 results.width = results.right - results.left; 799 results.height = results.bottom - results.top; 800 return results; 801 }; 802 803 // TODO(dsinclair): intersectRect_ can return false (which should actually 804 // be undefined) but we use finalDragBox without checking the return value 805 // which could potentially blowup. Fix this ..... 806 var finalDragBox = intersectRect_(clipRect, dragRect); 807 808 this.$.drag_box.style.left = finalDragBox.left + 'px'; 809 this.$.drag_box.style.width = finalDragBox.width + 'px'; 810 this.$.drag_box.style.top = finalDragBox.top + 'px'; 811 this.$.drag_box.style.height = finalDragBox.height + 'px'; 812 this.$.drag_box.style.whiteSpace = 'nowrap'; 813 814 var pixelRatio = window.devicePixelRatio || 1; 815 var canv = this.modelTrackContainer_.canvas; 816 var dt = this.viewport_.currentDisplayTransform; 817 var loWX = dt.xViewToWorld( 818 (loX - canv.offsetLeft) * pixelRatio); 819 var hiWX = dt.xViewToWorld( 820 (hiX - canv.offsetLeft) * pixelRatio); 821 822 this.$.drag_box.textContent = 823 tr.v.Unit.byName.timeDurationInMs.format(hiWX - loWX); 824 825 var e = new tr.b.Event('selectionChanging'); 826 e.loWX = loWX; 827 e.hiWX = hiWX; 828 this.dispatchEvent(e); 829 }, 830 831 onGridToggle_: function(left) { 832 var selection = this.brushingStateController_.selection; 833 var tb = left ? selection.bounds.min : selection.bounds.max; 834 835 // Toggle the grid off if the grid is on, the marker position is the same 836 // and the same element is selected (same timebase). 837 if (this.viewport_.gridEnabled && 838 this.viewport_.gridSide === left && 839 this.viewport_.gridInitialTimebase === tb) { 840 this.viewport_.gridside = undefined; 841 this.viewport_.gridEnabled = false; 842 this.viewport_.gridInitialTimebase = undefined; 843 return; 844 } 845 846 // Shift the timebase left until its just left of model_.bounds.min. 847 var numIntervalsSinceStart = Math.ceil((tb - this.model_.bounds.min) / 848 this.viewport_.gridStep_); 849 850 this.viewport_.gridEnabled = true; 851 this.viewport_.gridSide = left; 852 this.viewport_.gridInitialTimebase = tb; 853 this.viewport_.gridTimebase = tb - 854 (numIntervalsSinceStart + 1) * this.viewport_.gridStep_; 855 }, 856 857 storeLastMousePos_: function(e) { 858 this.lastMouseViewPos_ = this.extractRelativeMousePosition_(e); 859 }, 860 861 storeLastTouchPositions_: function(e) { 862 this.lastTouchViewPositions_ = this.extractRelativeTouchPositions_(e); 863 }, 864 865 extractRelativeMousePosition_: function(e) { 866 var canv = this.modelTrackContainer_.canvas; 867 return { 868 x: e.clientX - canv.offsetLeft, 869 y: e.clientY - canv.offsetTop 870 }; 871 }, 872 873 extractRelativeTouchPositions_: function(e) { 874 var canv = this.modelTrackContainer_.canvas; 875 876 var touches = []; 877 for (var i = 0; i < e.touches.length; ++i) { 878 touches.push({ 879 x: e.touches[i].clientX - canv.offsetLeft, 880 y: e.touches[i].clientY - canv.offsetTop 881 }); 882 } 883 return touches; 884 }, 885 886 storeInitialMouseDownPos_: function(e) { 887 888 var position = this.extractRelativeMousePosition_(e); 889 890 this.mouseViewPosAtMouseDown_.x = position.x; 891 this.mouseViewPosAtMouseDown_.y = position.y; 892 }, 893 894 focusElements_: function() { 895 this.$.hotkey_controller.childRequestsGeneralFocus(this); 896 }, 897 898 storeInitialInteractionPositionsAndFocus_: function(e) { 899 900 this.storeInitialMouseDownPos_(e); 901 this.storeLastMousePos_(e); 902 903 this.focusElements_(); 904 }, 905 906 onBeginPanScan_: function(e) { 907 var vp = this.viewport_; 908 this.viewportDisplayTransformAtMouseDown_ = 909 vp.currentDisplayTransform.clone(); 910 this.isPanningAndScanning_ = true; 911 912 this.storeInitialInteractionPositionsAndFocus_(e); 913 e.preventDefault(); 914 }, 915 916 onUpdatePanScan_: function(e) { 917 if (!this.isPanningAndScanning_) 918 return; 919 920 var viewWidth = this.viewWidth_; 921 922 var pixelRatio = window.devicePixelRatio || 1; 923 var xDeltaView = pixelRatio * (this.lastMouseViewPos_.x - 924 this.mouseViewPosAtMouseDown_.x); 925 926 var yDelta = this.lastMouseViewPos_.y - 927 this.mouseViewPosAtMouseDown_.y; 928 929 this.displayTransform_.set(this.viewportDisplayTransformAtMouseDown_); 930 this.displayTransform_.incrementPanXInViewUnits(xDeltaView); 931 this.displayTransform_.panY -= yDelta; 932 this.viewport_.setDisplayTransformImmediately(this.displayTransform_); 933 934 e.preventDefault(); 935 e.stopPropagation(); 936 937 this.storeLastMousePos_(e); 938 }, 939 940 onEndPanScan_: function(e) { 941 this.isPanningAndScanning_ = false; 942 943 this.storeLastMousePos_(e); 944 945 if (!e.isClick) 946 e.preventDefault(); 947 }, 948 949 onBeginSelection_: function(e) { 950 var canv = this.modelTrackContainer_.canvas; 951 var rect = this.modelTrack_.getBoundingClientRect(); 952 var canvRect = canv.getBoundingClientRect(); 953 954 var inside = rect && 955 e.clientX >= rect.left && 956 e.clientX < rect.right && 957 e.clientY >= rect.top && 958 e.clientY < rect.bottom && 959 e.clientX >= canvRect.left && 960 e.clientX < canvRect.right; 961 962 if (!inside) 963 return; 964 965 this.dragBeginEvent_ = e; 966 967 this.storeInitialInteractionPositionsAndFocus_(e); 968 e.preventDefault(); 969 }, 970 971 onUpdateSelection_: function(e) { 972 if (!this.dragBeginEvent_) 973 return; 974 975 // Update the drag box 976 this.dragBoxXStart_ = this.dragBeginEvent_.clientX; 977 this.dragBoxXEnd_ = e.clientX; 978 this.dragBoxYStart_ = this.dragBeginEvent_.clientY; 979 this.dragBoxYEnd_ = e.clientY; 980 this.setDragBoxPosition_(this.dragBoxXStart_, this.dragBoxYStart_, 981 this.dragBoxXEnd_, this.dragBoxYEnd_); 982 983 }, 984 985 onEndSelection_: function(e) { 986 e.preventDefault(); 987 988 if (!this.dragBeginEvent_) 989 return; 990 991 // Stop the dragging. 992 this.hideDragBox_(); 993 var eDown = this.dragBeginEvent_; 994 this.dragBeginEvent_ = undefined; 995 996 // Figure out extents of the drag. 997 var loY = Math.min(eDown.clientY, e.clientY); 998 var hiY = Math.max(eDown.clientY, e.clientY); 999 var loX = Math.min(eDown.clientX, e.clientX); 1000 var hiX = Math.max(eDown.clientX, e.clientX); 1001 1002 // Convert to worldspace. 1003 var canv = this.modelTrackContainer_.canvas; 1004 var worldOffset = canv.getBoundingClientRect().left; 1005 var loVX = loX - worldOffset; 1006 var hiVX = hiX - worldOffset; 1007 1008 // Figure out what has been selected. 1009 var selection = new tr.model.EventSet(); 1010 if (eDown.appendSelection) { 1011 var previousSelection = this.brushingStateController_.selection; 1012 if (previousSelection !== undefined) 1013 selection.addEventSet(previousSelection); 1014 } 1015 this.modelTrack_.addIntersectingEventsInRangeToSelection( 1016 loVX, hiVX, loY, hiY, selection); 1017 1018 // Activate the new selection. 1019 this.brushingStateController_.changeSelectionFromTimeline(selection); 1020 }, 1021 1022 onBeginZoom_: function(e) { 1023 this.isZooming_ = true; 1024 1025 this.storeInitialInteractionPositionsAndFocus_(e); 1026 e.preventDefault(); 1027 }, 1028 1029 onUpdateZoom_: function(e) { 1030 if (!this.isZooming_) 1031 return; 1032 var newPosition = this.extractRelativeMousePosition_(e); 1033 1034 var zoomScaleValue = 1 + (this.lastMouseViewPos_.y - 1035 newPosition.y) * 0.01; 1036 1037 this.zoomBy_(zoomScaleValue, false); 1038 this.storeLastMousePos_(e); 1039 }, 1040 1041 onEndZoom_: function(e) { 1042 this.isZooming_ = false; 1043 1044 if (!e.isClick) 1045 e.preventDefault(); 1046 }, 1047 1048 computeTouchCenter_: function(positions) { 1049 var xSum = 0; 1050 var ySum = 0; 1051 for (var i = 0; i < positions.length; ++i) { 1052 xSum += positions[i].x; 1053 ySum += positions[i].y; 1054 } 1055 return { 1056 x: xSum / positions.length, 1057 y: ySum / positions.length 1058 }; 1059 }, 1060 1061 computeTouchSpan_: function(positions) { 1062 var xMin = Number.MAX_VALUE; 1063 var yMin = Number.MAX_VALUE; 1064 var xMax = Number.MIN_VALUE; 1065 var yMax = Number.MIN_VALUE; 1066 for (var i = 0; i < positions.length; ++i) { 1067 xMin = Math.min(xMin, positions[i].x); 1068 yMin = Math.min(yMin, positions[i].y); 1069 xMax = Math.max(xMax, positions[i].x); 1070 yMax = Math.max(yMax, positions[i].y); 1071 } 1072 return Math.sqrt((xMin - xMax) * (xMin - xMax) + 1073 (yMin - yMax) * (yMin - yMax)); 1074 }, 1075 1076 onUpdateTransformForTouch_: function(e) { 1077 var newPositions = this.extractRelativeTouchPositions_(e); 1078 var currentPositions = this.lastTouchViewPositions_; 1079 1080 var newCenter = this.computeTouchCenter_(newPositions); 1081 var currentCenter = this.computeTouchCenter_(currentPositions); 1082 1083 var newSpan = this.computeTouchSpan_(newPositions); 1084 var currentSpan = this.computeTouchSpan_(currentPositions); 1085 1086 var vp = this.viewport_; 1087 var viewWidth = this.viewWidth_; 1088 var pixelRatio = window.devicePixelRatio || 1; 1089 1090 var xDelta = pixelRatio * (newCenter.x - currentCenter.x); 1091 var yDelta = newCenter.y - currentCenter.y; 1092 var zoomScaleValue = currentSpan > 10 ? newSpan / currentSpan : 1; 1093 1094 var viewFocus = pixelRatio * newCenter.x; 1095 var worldFocus = vp.currentDisplayTransform.xViewToWorld(viewFocus); 1096 1097 this.displayTransform_.set(vp.currentDisplayTransform); 1098 this.displayTransform_.scaleX *= zoomScaleValue; 1099 this.displayTransform_.xPanWorldPosToViewPos( 1100 worldFocus, viewFocus, viewWidth); 1101 this.displayTransform_.incrementPanXInViewUnits(xDelta); 1102 this.displayTransform_.panY -= yDelta; 1103 vp.setDisplayTransformImmediately(this.displayTransform_); 1104 this.storeLastTouchPositions_(e); 1105 }, 1106 1107 initHintText_: function() { 1108 this.$.hint_text.style.display = 'none'; 1109 1110 this.pendingHintTextClearTimeout_ = undefined; 1111 }, 1112 1113 showHintText_: function(text) { 1114 if (this.pendingHintTextClearTimeout_) { 1115 window.clearTimeout(this.pendingHintTextClearTimeout_); 1116 this.pendingHintTextClearTimeout_ = undefined; 1117 } 1118 this.pendingHintTextClearTimeout_ = setTimeout( 1119 this.hideHintText_.bind(this), 1000); 1120 this.$.hint_text.textContent = text; 1121 this.$.hint_text.style.display = ''; 1122 }, 1123 1124 hideHintText_: function() { 1125 this.pendingHintTextClearTimeout_ = undefined; 1126 this.$.hint_text.style.display = 'none'; 1127 } 1128 }); 1129 </script> 1130</polymer-element> 1131