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/model/event_set.html"> 10<link rel="import" href="/tracing/ui/base/animation.html"> 11<link rel="import" href="/tracing/ui/base/animation_controller.html"> 12<link rel="import" href="/tracing/ui/base/dom_helpers.html"> 13<link rel="import" href="/tracing/ui/base/draw_helpers.html"> 14<link rel="import" href="/tracing/ui/timeline_interest_range.html"> 15<link rel="import" href="/tracing/ui/timeline_display_transform.html"> 16<link rel="import" href="/tracing/ui/tracks/container_to_track_map.html"> 17<link rel="import" href="/tracing/ui/tracks/event_to_track_map.html"> 18 19<script> 20'use strict'; 21 22/** 23 * @fileoverview Code for the viewport. 24 */ 25tr.exportTo('tr.ui', function() { 26 var TimelineDisplayTransform = tr.ui.TimelineDisplayTransform; 27 var TimelineInterestRange = tr.ui.TimelineInterestRange; 28 29 /** 30 * The TimelineViewport manages the transform used for navigating 31 * within the timeline. It is a simple transform: 32 * x' = (x+pan) * scale 33 * 34 * The timeline code tries to avoid directly accessing this transform, 35 * instead using this class to do conversion between world and viewspace, 36 * as well as the math for centering the viewport in various interesting 37 * ways. 38 * 39 * @constructor 40 * @extends {tr.b.EventTarget} 41 */ 42 function TimelineViewport(parentEl) { 43 this.parentEl_ = parentEl; 44 this.modelTrackContainer_ = undefined; 45 this.currentDisplayTransform_ = new TimelineDisplayTransform(); 46 this.initAnimationController_(); 47 48 // Flow events 49 this.showFlowEvents_ = false; 50 51 // Highlights. 52 this.highlightVSync_ = false; 53 54 // High details. 55 this.highDetails_ = false; 56 57 // Grid system. 58 this.gridTimebase_ = 0; 59 this.gridStep_ = 1000 / 60; 60 this.gridEnabled_ = false; 61 62 // Init logic. 63 this.hasCalledSetupFunction_ = false; 64 65 this.onResize_ = this.onResize_.bind(this); 66 this.onModelTrackControllerScroll_ = 67 this.onModelTrackControllerScroll_.bind(this); 68 69 // The following code uses an interval to detect when the parent element 70 // is attached to the document. That is a trigger to run the setup function 71 // and install a resize listener. 72 this.checkForAttachInterval_ = setInterval( 73 this.checkForAttach_.bind(this), 250); 74 75 this.majorMarkPositions = []; 76 this.interestRange_ = new TimelineInterestRange(this); 77 78 this.eventToTrackMap_ = new tr.ui.tracks.EventToTrackMap(); 79 this.containerToTrackMap = new tr.ui.tracks.ContainerToTrackMap(); 80 } 81 82 TimelineViewport.prototype = { 83 __proto__: tr.b.EventTarget.prototype, 84 85 /** 86 * Allows initialization of the viewport when the viewport's parent element 87 * has been attached to the document and given a size. 88 * @param {Function} fn Function to call when the viewport can be safely 89 * initialized. 90 */ 91 setWhenPossible: function(fn) { 92 this.pendingSetFunction_ = fn; 93 }, 94 95 /** 96 * @return {boolean} Whether the current timeline is attached to the 97 * document. 98 */ 99 get isAttachedToDocumentOrInTestMode() { 100 // Allow not providing a parent element, used by tests. 101 if (this.parentEl_ === undefined) 102 return; 103 return tr.ui.b.isElementAttachedToDocument(this.parentEl_); 104 }, 105 106 onResize_: function() { 107 this.dispatchChangeEvent(); 108 }, 109 110 /** 111 * Checks whether the parentNode is attached to the document. 112 * When it is, it installs the iframe-based resize detection hook 113 * and then runs the pendingSetFunction_, if present. 114 */ 115 checkForAttach_: function() { 116 if (!this.isAttachedToDocumentOrInTestMode || this.clientWidth == 0) 117 return; 118 119 if (!this.iframe_) { 120 this.iframe_ = document.createElement('iframe'); 121 this.iframe_.style.cssText = 122 'position:absolute;width:100%;height:0;border:0;visibility:hidden;'; 123 this.parentEl_.appendChild(this.iframe_); 124 125 this.iframe_.contentWindow.addEventListener('resize', this.onResize_); 126 } 127 128 var curSize = this.parentEl_.clientWidth + 'x' + 129 this.parentEl_.clientHeight; 130 if (this.pendingSetFunction_) { 131 this.lastSize_ = curSize; 132 try { 133 this.pendingSetFunction_(); 134 } catch (ex) { 135 console.log('While running setWhenPossible:', 136 ex.message ? ex.message + '\n' + ex.stack : ex.stack); 137 } 138 this.pendingSetFunction_ = undefined; 139 } 140 141 window.clearInterval(this.checkForAttachInterval_); 142 this.checkForAttachInterval_ = undefined; 143 }, 144 145 /** 146 * Fires the change event on this viewport. Used to notify listeners 147 * to redraw when the underlying model has been mutated. 148 */ 149 dispatchChangeEvent: function() { 150 tr.b.dispatchSimpleEvent(this, 'change'); 151 }, 152 153 detach: function() { 154 if (this.checkForAttachInterval_) { 155 window.clearInterval(this.checkForAttachInterval_); 156 this.checkForAttachInterval_ = undefined; 157 } 158 if (this.iframe_) { 159 this.iframe_.removeEventListener('resize', this.onResize_); 160 this.parentEl_.removeChild(this.iframe_); 161 } 162 }, 163 164 initAnimationController_: function() { 165 this.dtAnimationController_ = new tr.ui.b.AnimationController(); 166 this.dtAnimationController_.addEventListener( 167 'didtick', function(e) { 168 this.onCurentDisplayTransformChange_(e.oldTargetState); 169 }.bind(this)); 170 171 var that = this; 172 this.dtAnimationController_.target = { 173 get panX() { 174 return that.currentDisplayTransform_.panX; 175 }, 176 177 set panX(panX) { 178 that.currentDisplayTransform_.panX = panX; 179 }, 180 181 get panY() { 182 return that.currentDisplayTransform_.panY; 183 }, 184 185 set panY(panY) { 186 that.currentDisplayTransform_.panY = panY; 187 }, 188 189 get scaleX() { 190 return that.currentDisplayTransform_.scaleX; 191 }, 192 193 set scaleX(scaleX) { 194 that.currentDisplayTransform_.scaleX = scaleX; 195 }, 196 197 cloneAnimationState: function() { 198 return that.currentDisplayTransform_.clone(); 199 }, 200 201 xPanWorldPosToViewPos: function(xWorld, xView) { 202 that.currentDisplayTransform_.xPanWorldPosToViewPos( 203 xWorld, xView, that.modelTrackContainer_.canvas.clientWidth); 204 } 205 }; 206 }, 207 208 get currentDisplayTransform() { 209 return this.currentDisplayTransform_; 210 }, 211 212 setDisplayTransformImmediately: function(displayTransform) { 213 this.dtAnimationController_.cancelActiveAnimation(); 214 215 var oldDisplayTransform = 216 this.dtAnimationController_.target.cloneAnimationState(); 217 this.currentDisplayTransform_.set(displayTransform); 218 this.onCurentDisplayTransformChange_(oldDisplayTransform); 219 }, 220 221 queueDisplayTransformAnimation: function(animation) { 222 if (!(animation instanceof tr.ui.b.Animation)) 223 throw new Error('animation must be instanceof tr.ui.b.Animation'); 224 this.dtAnimationController_.queueAnimation(animation); 225 }, 226 227 onCurentDisplayTransformChange_: function(oldDisplayTransform) { 228 // Ensure panY stays clamped in the track container's scroll range. 229 if (this.modelTrackContainer_) { 230 this.currentDisplayTransform.panY = tr.b.clamp( 231 this.currentDisplayTransform.panY, 232 0, 233 this.modelTrackContainer_.scrollHeight - 234 this.modelTrackContainer_.clientHeight); 235 } 236 237 var changed = !this.currentDisplayTransform.equals(oldDisplayTransform); 238 var yChanged = this.currentDisplayTransform.panY !== 239 oldDisplayTransform.panY; 240 if (yChanged) 241 this.modelTrackContainer_.scrollTop = this.currentDisplayTransform.panY; 242 if (changed) 243 this.dispatchChangeEvent(); 244 }, 245 246 onModelTrackControllerScroll_: function(e) { 247 if (this.dtAnimationController_.activeAnimation && 248 this.dtAnimationController_.activeAnimation.affectsPanY) 249 this.dtAnimationController_.cancelActiveAnimation(); 250 var panY = this.modelTrackContainer_.scrollTop; 251 this.currentDisplayTransform_.panY = panY; 252 }, 253 254 get modelTrackContainer() { 255 return this.modelTrackContainer_; 256 }, 257 258 set modelTrackContainer(m) { 259 if (this.modelTrackContainer_) 260 this.modelTrackContainer_.removeEventListener('scroll', 261 this.onModelTrackControllerScroll_); 262 263 this.modelTrackContainer_ = m; 264 this.modelTrackContainer_.addEventListener('scroll', 265 this.onModelTrackControllerScroll_); 266 }, 267 268 get showFlowEvents() { 269 return this.showFlowEvents_; 270 }, 271 272 set showFlowEvents(showFlowEvents) { 273 this.showFlowEvents_ = showFlowEvents; 274 this.dispatchChangeEvent(); 275 }, 276 277 get highlightVSync() { 278 return this.highlightVSync_; 279 }, 280 281 set highlightVSync(highlightVSync) { 282 this.highlightVSync_ = highlightVSync; 283 this.dispatchChangeEvent(); 284 }, 285 286 get highDetails() { 287 return this.highDetails_; 288 }, 289 290 set highDetails(highDetails) { 291 this.highDetails_ = highDetails; 292 this.dispatchChangeEvent(); 293 }, 294 295 get gridEnabled() { 296 return this.gridEnabled_; 297 }, 298 299 set gridEnabled(enabled) { 300 if (this.gridEnabled_ == enabled) 301 return; 302 303 this.gridEnabled_ = enabled && true; 304 this.dispatchChangeEvent(); 305 }, 306 307 get gridTimebase() { 308 return this.gridTimebase_; 309 }, 310 311 set gridTimebase(timebase) { 312 if (this.gridTimebase_ == timebase) 313 return; 314 this.gridTimebase_ = timebase; 315 this.dispatchChangeEvent(); 316 }, 317 318 get gridStep() { 319 return this.gridStep_; 320 }, 321 322 get interestRange() { 323 return this.interestRange_; 324 }, 325 326 drawMajorMarkLines: function(ctx) { 327 // Apply subpixel translate to get crisp lines. 328 // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/ 329 ctx.save(); 330 ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0); 331 332 ctx.beginPath(); 333 for (var idx in this.majorMarkPositions) { 334 var x = Math.floor(this.majorMarkPositions[idx]); 335 tr.ui.b.drawLine(ctx, x, 0, x, ctx.canvas.height); 336 } 337 ctx.strokeStyle = '#ddd'; 338 ctx.stroke(); 339 340 ctx.restore(); 341 }, 342 343 drawGridLines: function(ctx, viewLWorld, viewRWorld) { 344 if (!this.gridEnabled) 345 return; 346 347 var dt = this.currentDisplayTransform; 348 var x = this.gridTimebase; 349 350 // Apply subpixel translate to get crisp lines. 351 // http://www.mobtowers.com/html5-canvas-crisp-lines-every-time/ 352 ctx.save(); 353 ctx.translate((Math.round(ctx.lineWidth) % 2) / 2, 0); 354 355 ctx.beginPath(); 356 while (x < viewRWorld) { 357 if (x >= viewLWorld) { 358 // Do conversion to viewspace here rather than on 359 // x to avoid precision issues. 360 var vx = Math.floor(dt.xWorldToView(x)); 361 tr.ui.b.drawLine(ctx, vx, 0, vx, ctx.canvas.height); 362 } 363 364 x += this.gridStep; 365 } 366 ctx.strokeStyle = 'rgba(255, 0, 0, 0.25)'; 367 ctx.stroke(); 368 369 ctx.restore(); 370 }, 371 372 /** 373 * Helper for selection previous or next. 374 * @param {boolean} offset If positive, select one forward (next). 375 * Else, select previous. 376 * 377 * @return {boolean} true if current selection changed. 378 */ 379 getShiftedSelection: function(selection, offset) { 380 var newSelection = new tr.model.EventSet(); 381 for (var i = 0; i < selection.length; i++) { 382 var event = selection[i]; 383 384 // If this is a flow event, then move to its slice based on the 385 // offset direction. 386 if (event instanceof tr.model.FlowEvent) { 387 if (offset > 0) { 388 newSelection.push(event.endSlice); 389 } else if (offset < 0) { 390 newSelection.push(event.startSlice); 391 } else { 392 /* Do nothing. Zero offsets don't do anything. */ 393 } 394 continue; 395 } 396 397 var track = this.trackForEvent(event); 398 track.addEventNearToProvidedEventToSelection( 399 event, offset, newSelection); 400 } 401 402 if (newSelection.length == 0) 403 return undefined; 404 return newSelection; 405 }, 406 407 rebuildEventToTrackMap: function() { 408 // TODO(charliea): Make the event to track map have a similar interface 409 // to the container to track map so that we can just clear() here. 410 this.eventToTrackMap_ = new tr.ui.tracks.EventToTrackMap(); 411 this.modelTrackContainer_.addEventsToTrackMap(this.eventToTrackMap_); 412 }, 413 414 rebuildContainerToTrackMap: function() { 415 this.containerToTrackMap.clear(); 416 this.modelTrackContainer_.addContainersToTrackMap( 417 this.containerToTrackMap); 418 }, 419 420 trackForEvent: function(event) { 421 return this.eventToTrackMap_[event.guid]; 422 } 423 }; 424 425 return { 426 TimelineViewport: TimelineViewport 427 }; 428}); 429</script> 430