| OLD | NEW |
| (Empty) |
| 1 // Copyright (c) 2011 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 * @fileoverview Interactive visualizaiton of TimelineModel objects | |
| 7 * based loosely on gantt charts. Each thread in the TimelineModel is given a | |
| 8 * set of TimelineTracks, one per subrow in the thread. The Timeline class | |
| 9 * acts as a controller, creating the individual tracks, while TimelineTracks | |
| 10 * do actual drawing. | |
| 11 * | |
| 12 * Visually, the Timeline produces (prettier) visualizations like the following: | |
| 13 * Thread1: AAAAAAAAAA AAAAA | |
| 14 * BBBB BB | |
| 15 * Thread2: CCCCCC CCCCC | |
| 16 * | |
| 17 */ | |
| 18 cr.define('gpu', function() { | |
| 19 | |
| 20 /** | |
| 21 * The TimelineViewport manages the transform used for navigating | |
| 22 * within the timeline. It is a simple transform: | |
| 23 * x' = (x+pan) * scale | |
| 24 * | |
| 25 * The timeline code tries to avoid directly accessing this transform, | |
| 26 * instead using this class to do conversion between world and view space, | |
| 27 * as well as the math for centering the viewport in various interesting | |
| 28 * ways. | |
| 29 * | |
| 30 * @constructor | |
| 31 * @extends {cr.EventTarget} | |
| 32 */ | |
| 33 function TimelineViewport() { | |
| 34 this.scaleX_ = 1; | |
| 35 this.panX_ = 0; | |
| 36 this.gridTimebase_ = 0; | |
| 37 this.gridStep_ = 1000 / 60; | |
| 38 this.gridEnabled_ = false; | |
| 39 } | |
| 40 | |
| 41 TimelineViewport.prototype = { | |
| 42 __proto__: cr.EventTarget.prototype, | |
| 43 | |
| 44 get scaleX() { | |
| 45 return this.scaleX_; | |
| 46 }, | |
| 47 set scaleX(s) { | |
| 48 var changed = this.scaleX_ != s; | |
| 49 if (changed) { | |
| 50 this.scaleX_ = s; | |
| 51 cr.dispatchSimpleEvent(this, 'change'); | |
| 52 } | |
| 53 }, | |
| 54 | |
| 55 get panX() { | |
| 56 return this.panX_; | |
| 57 }, | |
| 58 set panX(p) { | |
| 59 var changed = this.panX_ != p; | |
| 60 if (changed) { | |
| 61 this.panX_ = p; | |
| 62 cr.dispatchSimpleEvent(this, 'change'); | |
| 63 } | |
| 64 }, | |
| 65 | |
| 66 setPanAndScale: function(p, s) { | |
| 67 var changed = this.scaleX_ != s || this.panX_ != p; | |
| 68 if (changed) { | |
| 69 this.scaleX_ = s; | |
| 70 this.panX_ = p; | |
| 71 cr.dispatchSimpleEvent(this, 'change'); | |
| 72 } | |
| 73 }, | |
| 74 | |
| 75 xWorldToView: function(x) { | |
| 76 return (x + this.panX_) * this.scaleX_; | |
| 77 }, | |
| 78 | |
| 79 xWorldVectorToView: function(x) { | |
| 80 return x * this.scaleX_; | |
| 81 }, | |
| 82 | |
| 83 xViewToWorld: function(x) { | |
| 84 return (x / this.scaleX_) - this.panX_; | |
| 85 }, | |
| 86 | |
| 87 xViewVectorToWorld: function(x) { | |
| 88 return x / this.scaleX_; | |
| 89 }, | |
| 90 | |
| 91 xPanWorldPosToViewPos: function(worldX, viewX, viewWidth) { | |
| 92 if (typeof viewX == 'string') { | |
| 93 if (viewX == 'left') { | |
| 94 viewX = 0; | |
| 95 } else if (viewX == 'center') { | |
| 96 viewX = viewWidth / 2; | |
| 97 } else if (viewX == 'right') { | |
| 98 viewX = viewWidth - 1; | |
| 99 } else { | |
| 100 throw Error('unrecognized string for viewPos. left|center|right'); | |
| 101 } | |
| 102 } | |
| 103 this.panX = (viewX / this.scaleX_) - worldX; | |
| 104 }, | |
| 105 | |
| 106 get gridEnabled() { | |
| 107 return this.gridEnabled_; | |
| 108 }, | |
| 109 | |
| 110 set gridEnabled(enabled) { | |
| 111 if (this.gridEnabled_ == enabled) | |
| 112 return; | |
| 113 this.gridEnabled_ = enabled && true; | |
| 114 cr.dispatchSimpleEvent(this, 'change'); | |
| 115 }, | |
| 116 | |
| 117 get gridTimebase() { | |
| 118 return this.gridTimebase_; | |
| 119 }, | |
| 120 | |
| 121 set gridTimebase(timebase) { | |
| 122 if (this.gridTimebase_ == timebase) | |
| 123 return; | |
| 124 this.gridTimebase_ = timebase; | |
| 125 cr.dispatchSimpleEvent(this, 'change'); | |
| 126 }, | |
| 127 | |
| 128 get gridStep() { | |
| 129 return this.gridStep_; | |
| 130 }, | |
| 131 | |
| 132 applyTransformToCanavs: function(ctx) { | |
| 133 ctx.transform(this.scaleX_, 0, 0, 1, this.panX_ * this.scaleX_, 0); | |
| 134 } | |
| 135 }; | |
| 136 | |
| 137 /** | |
| 138 * Renders a TimelineModel into a div element, making one | |
| 139 * TimelineTrack for each subrow in each thread of the model, managing | |
| 140 * overall track layout, and handling user interaction with the | |
| 141 * viewport. | |
| 142 * | |
| 143 * @constructor | |
| 144 * @extends {HTMLDivElement} | |
| 145 */ | |
| 146 Timeline = cr.ui.define('div'); | |
| 147 | |
| 148 Timeline.prototype = { | |
| 149 __proto__: HTMLDivElement.prototype, | |
| 150 | |
| 151 model_: null, | |
| 152 | |
| 153 decorate: function() { | |
| 154 this.classList.add('timeline'); | |
| 155 this.needsViewportReset_ = false; | |
| 156 | |
| 157 this.viewport_ = new TimelineViewport(); | |
| 158 this.viewport_.addEventListener('change', | |
| 159 this.viewportChange_.bind(this)); | |
| 160 | |
| 161 this.invalidatePending_ = false; | |
| 162 | |
| 163 this.tracks_ = this.ownerDocument.createElement('div'); | |
| 164 this.tracks_.invalidate = this.invalidate.bind(this); | |
| 165 this.appendChild(this.tracks_); | |
| 166 | |
| 167 this.dragBox_ = this.ownerDocument.createElement('div'); | |
| 168 this.dragBox_.className = 'timeline-drag-box'; | |
| 169 this.appendChild(this.dragBox_); | |
| 170 | |
| 171 // The following code uses a setInterval to monitor the timeline control | |
| 172 // for size changes. This is so that we can keep the canvas' bitmap size | |
| 173 // correctly synchronized with its presentation size. | |
| 174 // TODO(nduca): detect this in a more efficient way, e.g. iframe hack. | |
| 175 this.lastSize_ = this.clientWidth + 'x' + this.clientHeight; | |
| 176 this.ownerDocument.defaultView.setInterval(function() { | |
| 177 var curSize = this.clientWidth + 'x' + this.clientHeight; | |
| 178 if (this.clientWidth && curSize != this.lastSize_) { | |
| 179 this.lastSize_ = curSize; | |
| 180 this.onResize(); | |
| 181 } | |
| 182 }.bind(this), 250); | |
| 183 | |
| 184 document.addEventListener('keypress', this.onKeypress_.bind(this)); | |
| 185 document.addEventListener('keydown', this.onKeydown_.bind(this)); | |
| 186 document.addEventListener('mousedown', this.onMouseDown_.bind(this)); | |
| 187 document.addEventListener('mousemove', this.onMouseMove_.bind(this)); | |
| 188 document.addEventListener('mouseup', this.onMouseUp_.bind(this)); | |
| 189 document.addEventListener('dblclick', this.onDblClick_.bind(this)); | |
| 190 | |
| 191 this.lastMouseViewPos_ = {x: 0, y: 0}; | |
| 192 | |
| 193 this.selection_ = []; | |
| 194 }, | |
| 195 | |
| 196 get model() { | |
| 197 return this.model_; | |
| 198 }, | |
| 199 | |
| 200 set model(model) { | |
| 201 if (!model) | |
| 202 throw Error('Model cannot be null'); | |
| 203 if (this.model) { | |
| 204 throw Error('Cannot set model twice.'); | |
| 205 } | |
| 206 this.model_ = model; | |
| 207 | |
| 208 // Create tracks. | |
| 209 this.tracks_.textContent = ''; | |
| 210 var threads = model.getAllThreads(); | |
| 211 threads.sort(gpu.TimelineThread.compare); | |
| 212 for (var tI = 0; tI < threads.length; tI++) { | |
| 213 var thread = threads[tI]; | |
| 214 var track = new TimelineThreadTrack(); | |
| 215 track.thread = thread; | |
| 216 track.viewport = this.viewport_; | |
| 217 this.tracks_.appendChild(track); | |
| 218 | |
| 219 } | |
| 220 | |
| 221 this.needsViewportReset_ = true; | |
| 222 }, | |
| 223 | |
| 224 viewportChange_: function() { | |
| 225 this.invalidate(); | |
| 226 }, | |
| 227 | |
| 228 invalidate: function() { | |
| 229 if (this.invalidatePending_) | |
| 230 return; | |
| 231 this.invalidatePending_ = true; | |
| 232 window.setTimeout(function() { | |
| 233 this.invalidatePending_ = false; | |
| 234 this.redrawAllTracks_(); | |
| 235 }.bind(this), 0); | |
| 236 }, | |
| 237 | |
| 238 onResize: function() { | |
| 239 for (var i = 0; i < this.tracks_.children.length; ++i) { | |
| 240 var track = this.tracks_.children[i]; | |
| 241 track.onResize(); | |
| 242 } | |
| 243 }, | |
| 244 | |
| 245 redrawAllTracks_: function() { | |
| 246 if (this.needsViewportReset_ && this.clientWidth != 0) { | |
| 247 this.needsViewportReset_ = false; | |
| 248 /* update viewport */ | |
| 249 var rangeTimestamp = this.model_.maxTimestamp - | |
| 250 this.model_.minTimestamp; | |
| 251 var w = this.firstCanvas.width; | |
| 252 console.log('viewport was reset with w=', w); | |
| 253 var scaleX = w / rangeTimestamp; | |
| 254 var panX = -this.model_.minTimestamp; | |
| 255 this.viewport_.setPanAndScale(panX, scaleX); | |
| 256 } | |
| 257 for (var i = 0; i < this.tracks_.children.length; ++i) { | |
| 258 this.tracks_.children[i].redraw(); | |
| 259 } | |
| 260 }, | |
| 261 | |
| 262 updateChildViewports_: function() { | |
| 263 for (var cI = 0; cI < this.tracks_.children.length; ++cI) { | |
| 264 var child = this.tracks_.children[cI]; | |
| 265 child.setViewport(this.panX, this.scaleX); | |
| 266 } | |
| 267 }, | |
| 268 | |
| 269 onKeypress_: function(e) { | |
| 270 var vp = this.viewport_; | |
| 271 if (!this.firstCanvas) | |
| 272 return; | |
| 273 var viewWidth = this.firstCanvas.clientWidth; | |
| 274 var curMouseV, curCenterW; | |
| 275 switch (e.keyCode) { | |
| 276 case 101: // e | |
| 277 var vX = this.lastMouseViewPos_.x; | |
| 278 var wX = vp.xViewToWorld(this.lastMouseViewPos_.x); | |
| 279 var distFromCenter = vX - (viewWidth / 2); | |
| 280 var percFromCenter = distFromCenter / viewWidth; | |
| 281 var percFromCenterSq = percFromCenter * percFromCenter; | |
| 282 vp.xPanWorldPosToViewPos(wX, 'center', viewWidth); | |
| 283 break; | |
| 284 case 119: // w | |
| 285 this.zoomBy_(1.5); | |
| 286 break; | |
| 287 case 115: // s | |
| 288 this.zoomBy_(1 / 1.5); | |
| 289 break; | |
| 290 case 103: // g | |
| 291 this.onGridToggle_(true); | |
| 292 break; | |
| 293 case 71: // G | |
| 294 this.onGridToggle_(false); | |
| 295 break; | |
| 296 case 87: // W | |
| 297 this.zoomBy_(10); | |
| 298 break; | |
| 299 case 83: // S | |
| 300 this.zoomBy_(1 / 10); | |
| 301 break; | |
| 302 case 97: // a | |
| 303 vp.panX += vp.xViewVectorToWorld(viewWidth * 0.1); | |
| 304 break; | |
| 305 case 100: // d | |
| 306 vp.panX -= vp.xViewVectorToWorld(viewWidth * 0.1); | |
| 307 break; | |
| 308 case 65: // A | |
| 309 vp.panX += vp.xViewVectorToWorld(viewWidth * 0.5); | |
| 310 break; | |
| 311 case 68: // D | |
| 312 vp.panX -= vp.xViewVectorToWorld(viewWidth * 0.5); | |
| 313 break; | |
| 314 } | |
| 315 }, | |
| 316 | |
| 317 // Not all keys send a keypress. | |
| 318 onKeydown_: function(e) { | |
| 319 switch (e.keyCode) { | |
| 320 case 37: // left arrow | |
| 321 this.selectPrevious_(e); | |
| 322 e.preventDefault(); | |
| 323 break; | |
| 324 case 39: // right arrow | |
| 325 this.selectNext_(e); | |
| 326 e.preventDefault(); | |
| 327 break; | |
| 328 case 9: // TAB | |
| 329 if (e.shiftKey) | |
| 330 this.selectPrevious_(e); | |
| 331 else | |
| 332 this.selectNext_(e); | |
| 333 e.preventDefault(); | |
| 334 break; | |
| 335 } | |
| 336 }, | |
| 337 | |
| 338 /** | |
| 339 * Zoom in or out on the timeline by the given scale factor. | |
| 340 * @param {integer} scale The scale factor to apply. If <1, zooms out. | |
| 341 */ | |
| 342 zoomBy_: function(scale) { | |
| 343 if (!this.firstCanvas) | |
| 344 return; | |
| 345 var vp = this.viewport_; | |
| 346 var viewWidth = this.firstCanvas.clientWidth; | |
| 347 var curMouseV = this.lastMouseViewPos_.x; | |
| 348 var curCenterW = vp.xViewToWorld(curMouseV); | |
| 349 vp.scaleX = vp.scaleX * scale; | |
| 350 vp.xPanWorldPosToViewPos(curCenterW, curMouseV, viewWidth); | |
| 351 }, | |
| 352 | |
| 353 /** Select the next slice on the timeline. Applies to each track. */ | |
| 354 selectNext_: function(e) { | |
| 355 this.selectAdjoining_(e, true); | |
| 356 }, | |
| 357 | |
| 358 /** Select the previous slice on the timeline. Applies to each track. */ | |
| 359 selectPrevious_: function(e) { | |
| 360 this.selectAdjoining_(e, false); | |
| 361 }, | |
| 362 | |
| 363 /** | |
| 364 * Helper for selection previous or next. | |
| 365 * @param {Event} The current event. | |
| 366 * @param {boolean} forwardp If true, select one forward (next). | |
| 367 * Else, select previous. | |
| 368 */ | |
| 369 selectAdjoining_: function(e, forwardp) { | |
| 370 var i, track, slice, adjoining; | |
| 371 var selection = []; | |
| 372 // Clear old selection; try and select next. | |
| 373 for (i = 0; i < this.selection_.length; ++i) { | |
| 374 adjoining = undefined; | |
| 375 this.selection_[i].slice.selected = false; | |
| 376 var track = this.selection_[i].track; | |
| 377 var slice = this.selection_[i].slice; | |
| 378 if (slice) { | |
| 379 if (forwardp) | |
| 380 adjoining = track.pickNext(slice); | |
| 381 else | |
| 382 adjoining = track.pickPrevious(slice); | |
| 383 } | |
| 384 if (adjoining != undefined) | |
| 385 selection.push({track: track, slice: adjoining}); | |
| 386 } | |
| 387 // Activate the new selection. | |
| 388 this.selection_ = selection; | |
| 389 for (i = 0; i < this.selection_.length; ++i) | |
| 390 this.selection_[i].slice.selected = true; | |
| 391 cr.dispatchSimpleEvent(this, 'selectionChange'); | |
| 392 this.invalidate(); // Cause tracks to redraw. | |
| 393 e.preventDefault(); | |
| 394 }, | |
| 395 | |
| 396 get keyHelp() { | |
| 397 return 'Keyboard shortcuts:\n' + | |
| 398 ' w/s : Zoom in/out (with shift: go faster)\n' + | |
| 399 ' a/d : Pan left/right\n' + | |
| 400 ' e : Center on mouse\n' + | |
| 401 ' g/G : Shows grid at the start/end of the selected task\n' + | |
| 402 ' <-,^TAB : Select previous event on current timeline\n' + | |
| 403 ' ->, TAB : Select next event on current timeline\n' + | |
| 404 '\n' + | |
| 405 'Dbl-click to zoom in; Shift dbl-click to zoom out\n'; | |
| 406 | |
| 407 | |
| 408 }, | |
| 409 | |
| 410 get selection() { | |
| 411 return this.selection_; | |
| 412 }, | |
| 413 | |
| 414 get firstCanvas() { | |
| 415 return this.tracks_.firstChild ? | |
| 416 this.tracks_.firstChild.firstCanvas : undefined; | |
| 417 }, | |
| 418 | |
| 419 showDragBox_: function() { | |
| 420 this.dragBox_.hidden = false; | |
| 421 }, | |
| 422 | |
| 423 hideDragBox_: function() { | |
| 424 this.dragBox_.style.left = '-1000px'; | |
| 425 this.dragBox_.style.top = '-1000px'; | |
| 426 this.dragBox_.style.width = 0; | |
| 427 this.dragBox_.style.height = 0; | |
| 428 this.dragBox_.hidden = true; | |
| 429 }, | |
| 430 | |
| 431 get dragBoxVisible_() { | |
| 432 return this.dragBox_.hidden == false; | |
| 433 }, | |
| 434 | |
| 435 setDragBoxPosition_: function(eDown, eCur) { | |
| 436 var loX = Math.min(eDown.clientX, eCur.clientX); | |
| 437 var hiX = Math.max(eDown.clientX, eCur.clientX); | |
| 438 var loY = Math.min(eDown.clientY, eCur.clientY); | |
| 439 var hiY = Math.max(eDown.clientY, eCur.clientY); | |
| 440 | |
| 441 this.dragBox_.style.left = loX + 'px'; | |
| 442 this.dragBox_.style.top = loY + 'px'; | |
| 443 this.dragBox_.style.width = hiX - loX + 'px'; | |
| 444 this.dragBox_.style.height = hiY - loY + 'px'; | |
| 445 | |
| 446 var canv = this.firstCanvas; | |
| 447 var loWX = this.viewport_.xViewToWorld(loX - canv.offsetLeft); | |
| 448 var hiWX = this.viewport_.xViewToWorld(hiX - canv.offsetLeft); | |
| 449 | |
| 450 var roundedDuration = Math.round((hiWX - loWX) * 100) / 100; | |
| 451 this.dragBox_.textContent = roundedDuration + 'ms'; | |
| 452 | |
| 453 var e = new cr.Event('selectionChanging'); | |
| 454 e.loWX = loWX; | |
| 455 e.hiWX = hiWX; | |
| 456 this.dispatchEvent(e); | |
| 457 }, | |
| 458 | |
| 459 onGridToggle_: function(left) { | |
| 460 var tb; | |
| 461 if (left) | |
| 462 tb = Math.min.apply(Math, this.selection_.map( | |
| 463 function(x) { return x.slice.start; })); | |
| 464 else | |
| 465 tb = Math.max.apply(Math, this.selection_.map( | |
| 466 function(x) { return x.slice.end; })); | |
| 467 | |
| 468 // Shift the timebase left until its just left of minTimestamp. | |
| 469 var numInterfvalsSinceStart = Math.ceil((tb - this.model_.minTimestamp) / | |
| 470 this.viewport_.gridStep_); | |
| 471 this.viewport_.gridTimebase = tb - | |
| 472 (numInterfvalsSinceStart + 1) * this.viewport_.gridStep_; | |
| 473 this.viewport_.gridEnabled = true; | |
| 474 }, | |
| 475 | |
| 476 onMouseDown_: function(e) { | |
| 477 if (e.clientX < this.offsetLeft || | |
| 478 e.clientX >= this.offsetLeft + this.offsetWidth || | |
| 479 e.clientY < this.offsetTop || | |
| 480 e.clientY >= this.offsetTop + this.offsetHeight) | |
| 481 return; | |
| 482 | |
| 483 var canv = this.firstCanvas; | |
| 484 var pos = { | |
| 485 x: e.clientX - canv.offsetLeft, | |
| 486 y: e.clientY - canv.offsetTop | |
| 487 }; | |
| 488 | |
| 489 var wX = this.viewport_.xViewToWorld(pos.x); | |
| 490 | |
| 491 this.dragBeginEvent_ = e; | |
| 492 e.preventDefault(); | |
| 493 }, | |
| 494 | |
| 495 onMouseMove_: function(e) { | |
| 496 if (!this.firstCanvas) | |
| 497 return; | |
| 498 var canv = this.firstCanvas; | |
| 499 var pos = { | |
| 500 x: e.clientX - canv.offsetLeft, | |
| 501 y: e.clientY - canv.offsetTop | |
| 502 }; | |
| 503 | |
| 504 // Remember position. Used during keyboard zooming. | |
| 505 this.lastMouseViewPos_ = pos; | |
| 506 | |
| 507 // Initiate the drag box if needed. | |
| 508 if (this.dragBeginEvent_ && !this.dragBoxVisible_) { | |
| 509 this.showDragBox_(); | |
| 510 this.setDragBoxPosition_(e, e); | |
| 511 } | |
| 512 | |
| 513 // Update the drag box | |
| 514 if (this.dragBeginEvent_) { | |
| 515 this.setDragBoxPosition_(this.dragBeginEvent_, e); | |
| 516 } | |
| 517 }, | |
| 518 | |
| 519 onMouseUp_: function(e) { | |
| 520 var i; | |
| 521 if (this.dragBeginEvent_) { | |
| 522 // Stop the dragging. | |
| 523 this.hideDragBox_(); | |
| 524 var eDown = this.dragBeginEvent_; | |
| 525 this.dragBeginEvent_ = null; | |
| 526 | |
| 527 // Figure out extents of the drag. | |
| 528 var loX = Math.min(eDown.clientX, e.clientX); | |
| 529 var hiX = Math.max(eDown.clientX, e.clientX); | |
| 530 var loY = Math.min(eDown.clientY, e.clientY); | |
| 531 var hiY = Math.max(eDown.clientY, e.clientY); | |
| 532 | |
| 533 // Convert to worldspace. | |
| 534 var canv = this.firstCanvas; | |
| 535 var loWX = this.viewport_.xViewToWorld(loX - canv.offsetLeft); | |
| 536 var hiWX = this.viewport_.xViewToWorld(hiX - canv.offsetLeft); | |
| 537 | |
| 538 // Clear old selection. | |
| 539 for (i = 0; i < this.selection_.length; ++i) { | |
| 540 this.selection_[i].slice.selected = false; | |
| 541 } | |
| 542 | |
| 543 // Figure out what has been hit. | |
| 544 var selection = []; | |
| 545 function addHit(type, track, slice) { | |
| 546 selection.push({track: track, slice: slice}); | |
| 547 } | |
| 548 for (i = 0; i < this.tracks_.children.length; ++i) { | |
| 549 var track = this.tracks_.children[i]; | |
| 550 | |
| 551 // Only check tracks that insersect the rect. | |
| 552 var trackClientRect = track.getBoundingClientRect(); | |
| 553 var a = Math.max(loY, trackClientRect.top); | |
| 554 var b = Math.min(hiY, trackClientRect.bottom); | |
| 555 if (a <= b) { | |
| 556 track.pickRange(loWX, hiWX, loY, hiY, addHit); | |
| 557 } | |
| 558 } | |
| 559 // Activate the new selection. | |
| 560 this.selection_ = selection; | |
| 561 cr.dispatchSimpleEvent(this, 'selectionChange'); | |
| 562 for (i = 0; i < this.selection_.length; ++i) { | |
| 563 this.selection_[i].slice.selected = true; | |
| 564 } | |
| 565 this.invalidate(); // Cause tracks to redraw. | |
| 566 } | |
| 567 }, | |
| 568 | |
| 569 onDblClick_: function(e) { | |
| 570 var scale = 4; | |
| 571 if (e.shiftKey) | |
| 572 scale = 1 / scale; | |
| 573 this.zoomBy_(scale); | |
| 574 e.preventDefault(); | |
| 575 }, | |
| 576 }; | |
| 577 | |
| 578 /** | |
| 579 * The TimelineModel being viewed by the timeline | |
| 580 * @type {TimelineModel} | |
| 581 */ | |
| 582 cr.defineProperty(Timeline, 'model', cr.PropertyKind.JS); | |
| 583 | |
| 584 return { | |
| 585 Timeline: Timeline | |
| 586 }; | |
| 587 }); | |
| OLD | NEW |