| 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 for (var tI = 0; tI < threads.length; tI++) { | |
| 212 var thread = threads[tI]; | |
| 213 var track = new TimelineThreadTrack(); | |
| 214 track.thread = thread; | |
| 215 track.viewport = this.viewport_; | |
| 216 this.tracks_.appendChild(track); | |
| 217 | |
| 218 } | |
| 219 | |
| 220 this.needsViewportReset_ = true; | |
| 221 }, | |
| 222 | |
| 223 viewportChange_: function() { | |
| 224 this.invalidate(); | |
| 225 }, | |
| 226 | |
| 227 invalidate: function() { | |
| 228 if (this.invalidatePending_) | |
| 229 return; | |
| 230 this.invalidatePending_ = true; | |
| 231 window.setTimeout(function() { | |
| 232 this.invalidatePending_ = false; | |
| 233 this.redrawAllTracks_(); | |
| 234 }.bind(this), 0); | |
| 235 }, | |
| 236 | |
| 237 onResize: function() { | |
| 238 for (var i = 0; i < this.tracks_.children.length; ++i) { | |
| 239 var track = this.tracks_.children[i]; | |
| 240 track.onResize(); | |
| 241 } | |
| 242 }, | |
| 243 | |
| 244 redrawAllTracks_: function() { | |
| 245 if (this.needsViewportReset_ && this.clientWidth != 0) { | |
| 246 this.needsViewportReset_ = false; | |
| 247 /* update viewport */ | |
| 248 var rangeTimestamp = this.model_.maxTimestamp - | |
| 249 this.model_.minTimestamp; | |
| 250 var w = this.firstCanvas.width; | |
| 251 console.log('viewport was reset with w=', w); | |
| 252 var scaleX = w / rangeTimestamp; | |
| 253 var panX = -this.model_.minTimestamp; | |
| 254 this.viewport_.setPanAndScale(panX, scaleX); | |
| 255 } | |
| 256 for (var i = 0; i < this.tracks_.children.length; ++i) { | |
| 257 this.tracks_.children[i].redraw(); | |
| 258 } | |
| 259 }, | |
| 260 | |
| 261 updateChildViewports_: function() { | |
| 262 for (var cI = 0; cI < this.tracks_.children.length; ++cI) { | |
| 263 var child = this.tracks_.children[cI]; | |
| 264 child.setViewport(this.panX, this.scaleX); | |
| 265 } | |
| 266 }, | |
| 267 | |
| 268 onKeypress_: function(e) { | |
| 269 var vp = this.viewport_; | |
| 270 if (!this.firstCanvas) | |
| 271 return; | |
| 272 var viewWidth = this.firstCanvas.clientWidth; | |
| 273 var curMouseV, curCenterW; | |
| 274 switch (e.keyCode) { | |
| 275 case 101: // e | |
| 276 var vX = this.lastMouseViewPos_.x; | |
| 277 var wX = vp.xViewToWorld(this.lastMouseViewPos_.x); | |
| 278 var distFromCenter = vX - (viewWidth / 2); | |
| 279 var percFromCenter = distFromCenter / viewWidth; | |
| 280 var percFromCenterSq = percFromCenter * percFromCenter; | |
| 281 vp.xPanWorldPosToViewPos(wX, 'center', viewWidth); | |
| 282 break; | |
| 283 case 119: // w | |
| 284 this.zoomBy_(1.5); | |
| 285 break; | |
| 286 case 115: // s | |
| 287 this.zoomBy_(1 / 1.5); | |
| 288 break; | |
| 289 case 103: // g | |
| 290 this.onGridToggle_(true); | |
| 291 break; | |
| 292 case 71: // G | |
| 293 this.onGridToggle_(false); | |
| 294 break; | |
| 295 case 87: // W | |
| 296 this.zoomBy_(10); | |
| 297 break; | |
| 298 case 83: // S | |
| 299 this.zoomBy_(1 / 10); | |
| 300 break; | |
| 301 case 97: // a | |
| 302 vp.panX += vp.xViewVectorToWorld(viewWidth * 0.1); | |
| 303 break; | |
| 304 case 100: // d | |
| 305 vp.panX -= vp.xViewVectorToWorld(viewWidth * 0.1); | |
| 306 break; | |
| 307 case 65: // A | |
| 308 vp.panX += vp.xViewVectorToWorld(viewWidth * 0.5); | |
| 309 break; | |
| 310 case 68: // D | |
| 311 vp.panX -= vp.xViewVectorToWorld(viewWidth * 0.5); | |
| 312 break; | |
| 313 } | |
| 314 }, | |
| 315 | |
| 316 // Not all keys send a keypress. | |
| 317 onKeydown_: function(e) { | |
| 318 switch (e.keyCode) { | |
| 319 case 37: // left arrow | |
| 320 this.selectPrevious_(e); | |
| 321 e.preventDefault(); | |
| 322 break; | |
| 323 case 39: // right arrow | |
| 324 this.selectNext_(e); | |
| 325 e.preventDefault(); | |
| 326 break; | |
| 327 case 9: // TAB | |
| 328 if (e.shiftKey) | |
| 329 this.selectPrevious_(e); | |
| 330 else | |
| 331 this.selectNext_(e); | |
| 332 e.preventDefault(); | |
| 333 break; | |
| 334 } | |
| 335 }, | |
| 336 | |
| 337 /** | |
| 338 * Zoom in or out on the timeline by the given scale factor. | |
| 339 * @param {integer} scale The scale factor to apply. If <1, zooms out. | |
| 340 */ | |
| 341 zoomBy_: function(scale) { | |
| 342 if (!this.firstCanvas) | |
| 343 return; | |
| 344 var vp = this.viewport_; | |
| 345 var viewWidth = this.firstCanvas.clientWidth; | |
| 346 var curMouseV = this.lastMouseViewPos_.x; | |
| 347 var curCenterW = vp.xViewToWorld(curMouseV); | |
| 348 vp.scaleX = vp.scaleX * scale; | |
| 349 vp.xPanWorldPosToViewPos(curCenterW, curMouseV, viewWidth); | |
| 350 }, | |
| 351 | |
| 352 /** Select the next slice on the timeline. Applies to each track. */ | |
| 353 selectNext_: function(e) { | |
| 354 this.selectAdjoining_(e, true); | |
| 355 }, | |
| 356 | |
| 357 /** Select the previous slice on the timeline. Applies to each track. */ | |
| 358 selectPrevious_: function(e) { | |
| 359 this.selectAdjoining_(e, false); | |
| 360 }, | |
| 361 | |
| 362 /** | |
| 363 * Helper for selection previous or next. | |
| 364 * @param {Event} The current event. | |
| 365 * @param {boolean} forwardp If true, select one forward (next). | |
| 366 * Else, select previous. | |
| 367 */ | |
| 368 selectAdjoining_: function(e, forwardp) { | |
| 369 var i, track, slice, adjoining; | |
| 370 var selection = []; | |
| 371 // Clear old selection; try and select next. | |
| 372 for (i = 0; i < this.selection_.length; ++i) { | |
| 373 adjoining = undefined; | |
| 374 this.selection_[i].slice.selected = false; | |
| 375 var track = this.selection_[i].track; | |
| 376 var slice = this.selection_[i].slice; | |
| 377 if (slice) { | |
| 378 if (forwardp) | |
| 379 adjoining = track.pickNext(slice); | |
| 380 else | |
| 381 adjoining = track.pickPrevious(slice); | |
| 382 } | |
| 383 if (adjoining != undefined) | |
| 384 selection.push({track: track, slice: adjoining}); | |
| 385 } | |
| 386 // Activate the new selection. | |
| 387 this.selection_ = selection; | |
| 388 for (i = 0; i < this.selection_.length; ++i) | |
| 389 this.selection_[i].slice.selected = true; | |
| 390 cr.dispatchSimpleEvent(this, 'selectionChange'); | |
| 391 this.invalidate(); // Cause tracks to redraw. | |
| 392 e.preventDefault(); | |
| 393 }, | |
| 394 | |
| 395 get keyHelp() { | |
| 396 return 'Keyboard shortcuts:\n' + | |
| 397 ' w/s : Zoom in/out (with shift: go faster)\n' + | |
| 398 ' a/d : Pan left/right\n' + | |
| 399 ' e : Center on mouse\n' + | |
| 400 ' g/G : Shows grid at the start/end of the selected task\n' + | |
| 401 ' <-,^TAB : Select previous event on current timeline\n' + | |
| 402 ' ->, TAB : Select next event on current timeline\n' + | |
| 403 '\n' + | |
| 404 'Dbl-click to zoom in; Shift dbl-click to zoom out\n'; | |
| 405 | |
| 406 | |
| 407 }, | |
| 408 | |
| 409 get selection() { | |
| 410 return this.selection_; | |
| 411 }, | |
| 412 | |
| 413 get firstCanvas() { | |
| 414 return this.tracks_.firstChild ? | |
| 415 this.tracks_.firstChild.firstCanvas : undefined; | |
| 416 }, | |
| 417 | |
| 418 showDragBox_: function() { | |
| 419 this.dragBox_.hidden = false; | |
| 420 }, | |
| 421 | |
| 422 hideDragBox_: function() { | |
| 423 this.dragBox_.style.left = '-1000px'; | |
| 424 this.dragBox_.style.top = '-1000px'; | |
| 425 this.dragBox_.style.width = 0; | |
| 426 this.dragBox_.style.height = 0; | |
| 427 this.dragBox_.hidden = true; | |
| 428 }, | |
| 429 | |
| 430 get dragBoxVisible_() { | |
| 431 return this.dragBox_.hidden == false; | |
| 432 }, | |
| 433 | |
| 434 setDragBoxPosition_: function(eDown, eCur) { | |
| 435 var loX = Math.min(eDown.clientX, eCur.clientX); | |
| 436 var hiX = Math.max(eDown.clientX, eCur.clientX); | |
| 437 var loY = Math.min(eDown.clientY, eCur.clientY); | |
| 438 var hiY = Math.max(eDown.clientY, eCur.clientY); | |
| 439 | |
| 440 this.dragBox_.style.left = loX + 'px'; | |
| 441 this.dragBox_.style.top = loY + 'px'; | |
| 442 this.dragBox_.style.width = hiX - loX + 'px'; | |
| 443 this.dragBox_.style.height = hiY - loY + 'px'; | |
| 444 | |
| 445 var canv = this.firstCanvas; | |
| 446 var loWX = this.viewport_.xViewToWorld(loX - canv.offsetLeft); | |
| 447 var hiWX = this.viewport_.xViewToWorld(hiX - canv.offsetLeft); | |
| 448 | |
| 449 var roundedDuration = Math.round((hiWX - loWX) * 100) / 100; | |
| 450 this.dragBox_.textContent = roundedDuration + 'ms'; | |
| 451 | |
| 452 var e = new cr.Event('selectionChanging'); | |
| 453 e.loWX = loWX; | |
| 454 e.hiWX = hiWX; | |
| 455 this.dispatchEvent(e); | |
| 456 }, | |
| 457 | |
| 458 onGridToggle_: function(left) { | |
| 459 var tb; | |
| 460 if (left) | |
| 461 tb = Math.min.apply(Math, this.selection_.map( | |
| 462 function(x) { return x.slice.start; })); | |
| 463 else | |
| 464 tb = Math.max.apply(Math, this.selection_.map( | |
| 465 function(x) { return x.slice.end; })); | |
| 466 | |
| 467 // Shift the timebase left until its just left of minTimestamp. | |
| 468 var numInterfvalsSinceStart = Math.ceil((tb - this.model_.minTimestamp) / | |
| 469 this.viewport_.gridStep_); | |
| 470 this.viewport_.gridTimebase = tb - | |
| 471 (numInterfvalsSinceStart + 1) * this.viewport_.gridStep_; | |
| 472 this.viewport_.gridEnabled = true; | |
| 473 }, | |
| 474 | |
| 475 onMouseDown_: function(e) { | |
| 476 if (e.clientX < this.offsetLeft || | |
| 477 e.clientX >= this.offsetLeft + this.offsetWidth || | |
| 478 e.clientY < this.offsetTop || | |
| 479 e.clientY >= this.offsetTop + this.offsetHeight) | |
| 480 return; | |
| 481 | |
| 482 var canv = this.firstCanvas; | |
| 483 var pos = { | |
| 484 x: e.clientX - canv.offsetLeft, | |
| 485 y: e.clientY - canv.offsetTop | |
| 486 }; | |
| 487 | |
| 488 var wX = this.viewport_.xViewToWorld(pos.x); | |
| 489 | |
| 490 this.dragBeginEvent_ = e; | |
| 491 e.preventDefault(); | |
| 492 }, | |
| 493 | |
| 494 onMouseMove_: function(e) { | |
| 495 if (!this.firstCanvas) | |
| 496 return; | |
| 497 var canv = this.firstCanvas; | |
| 498 var pos = { | |
| 499 x: e.clientX - canv.offsetLeft, | |
| 500 y: e.clientY - canv.offsetTop | |
| 501 }; | |
| 502 | |
| 503 // Remember position. Used during keyboard zooming. | |
| 504 this.lastMouseViewPos_ = pos; | |
| 505 | |
| 506 // Initiate the drag box if needed. | |
| 507 if (this.dragBeginEvent_ && !this.dragBoxVisible_) { | |
| 508 this.showDragBox_(); | |
| 509 this.setDragBoxPosition_(e, e); | |
| 510 } | |
| 511 | |
| 512 // Update the drag box | |
| 513 if (this.dragBeginEvent_) { | |
| 514 this.setDragBoxPosition_(this.dragBeginEvent_, e); | |
| 515 } | |
| 516 }, | |
| 517 | |
| 518 onMouseUp_: function(e) { | |
| 519 var i; | |
| 520 if (this.dragBeginEvent_) { | |
| 521 // Stop the dragging. | |
| 522 this.hideDragBox_(); | |
| 523 var eDown = this.dragBeginEvent_; | |
| 524 this.dragBeginEvent_ = null; | |
| 525 | |
| 526 // Figure out extents of the drag. | |
| 527 var loX = Math.min(eDown.clientX, e.clientX); | |
| 528 var hiX = Math.max(eDown.clientX, e.clientX); | |
| 529 var loY = Math.min(eDown.clientY, e.clientY); | |
| 530 var hiY = Math.max(eDown.clientY, e.clientY); | |
| 531 | |
| 532 // Convert to worldspace. | |
| 533 var canv = this.firstCanvas; | |
| 534 var loWX = this.viewport_.xViewToWorld(loX - canv.offsetLeft); | |
| 535 var hiWX = this.viewport_.xViewToWorld(hiX - canv.offsetLeft); | |
| 536 | |
| 537 // Clear old selection. | |
| 538 for (i = 0; i < this.selection_.length; ++i) { | |
| 539 this.selection_[i].slice.selected = false; | |
| 540 } | |
| 541 | |
| 542 // Figure out what has been hit. | |
| 543 var selection = []; | |
| 544 function addHit(type, track, slice) { | |
| 545 selection.push({track: track, slice: slice}); | |
| 546 } | |
| 547 for (i = 0; i < this.tracks_.children.length; ++i) { | |
| 548 var track = this.tracks_.children[i]; | |
| 549 | |
| 550 // Only check tracks that insersect the rect. | |
| 551 var trackClientRect = track.getBoundingClientRect(); | |
| 552 var a = Math.max(loY, trackClientRect.top); | |
| 553 var b = Math.min(hiY, trackClientRect.bottom); | |
| 554 if (a <= b) { | |
| 555 track.pickRange(loWX, hiWX, loY, hiY, addHit); | |
| 556 } | |
| 557 } | |
| 558 // Activate the new selection. | |
| 559 this.selection_ = selection; | |
| 560 cr.dispatchSimpleEvent(this, 'selectionChange'); | |
| 561 for (i = 0; i < this.selection_.length; ++i) { | |
| 562 this.selection_[i].slice.selected = true; | |
| 563 } | |
| 564 this.invalidate(); // Cause tracks to redraw. | |
| 565 } | |
| 566 }, | |
| 567 | |
| 568 onDblClick_: function(e) { | |
| 569 var scale = 4; | |
| 570 if (e.shiftKey) | |
| 571 scale = 1 / scale; | |
| 572 this.zoomBy_(scale); | |
| 573 e.preventDefault(); | |
| 574 }, | |
| 575 }; | |
| 576 | |
| 577 /** | |
| 578 * The TimelineModel being viewed by the timeline | |
| 579 * @type {TimelineModel} | |
| 580 */ | |
| 581 cr.defineProperty(Timeline, 'model', cr.PropertyKind.JS); | |
| 582 | |
| 583 return { | |
| 584 Timeline: Timeline | |
| 585 }; | |
| 586 }); | |
| OLD | NEW |