| OLD | NEW |
| (Empty) |
| 1 /* | |
| 2 * Copyright (C) 2013 Google Inc. All rights reserved. | |
| 3 * | |
| 4 * Redistribution and use in source and binary forms, with or without | |
| 5 * modification, are permitted provided that the following conditions are | |
| 6 * met: | |
| 7 * | |
| 8 * * Redistributions of source code must retain the above copyright | |
| 9 * notice, this list of conditions and the following disclaimer. | |
| 10 * * Redistributions in binary form must reproduce the above | |
| 11 * copyright notice, this list of conditions and the following disclaimer | |
| 12 * in the documentation and/or other materials provided with the | |
| 13 * distribution. | |
| 14 * * Neither the name of Google Inc. nor the names of its | |
| 15 * contributors may be used to endorse or promote products derived from | |
| 16 * this software without specific prior written permission. | |
| 17 * | |
| 18 * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS | |
| 19 * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT | |
| 20 * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR | |
| 21 * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT | |
| 22 * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, | |
| 23 * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT | |
| 24 * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, | |
| 25 * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY | |
| 26 * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT | |
| 27 * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE | |
| 28 * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. | |
| 29 */ | |
| 30 | |
| 31 /** | |
| 32 * @unrestricted | |
| 33 */ | |
| 34 UI.TimelineOverviewPane = class extends UI.VBox { | |
| 35 /** | |
| 36 * @param {string} prefix | |
| 37 */ | |
| 38 constructor(prefix) { | |
| 39 super(); | |
| 40 this.element.id = prefix + '-overview-pane'; | |
| 41 | |
| 42 this._overviewCalculator = new UI.TimelineOverviewCalculator(); | |
| 43 this._overviewGrid = new UI.OverviewGrid(prefix); | |
| 44 this.element.appendChild(this._overviewGrid.element); | |
| 45 this._cursorArea = this._overviewGrid.element.createChild('div', 'overview-g
rid-cursor-area'); | |
| 46 this._cursorElement = this._overviewGrid.element.createChild('div', 'overvie
w-grid-cursor-position'); | |
| 47 this._cursorArea.addEventListener('mousemove', this._onMouseMove.bind(this),
true); | |
| 48 this._cursorArea.addEventListener('mouseleave', this._hideCursor.bind(this),
true); | |
| 49 | |
| 50 this._overviewGrid.setResizeEnabled(false); | |
| 51 this._overviewGrid.addEventListener(UI.OverviewGrid.Events.WindowChanged, th
is._onWindowChanged, this); | |
| 52 this._overviewGrid.setClickHandler(this._onClick.bind(this)); | |
| 53 this._overviewControls = []; | |
| 54 this._markers = new Map(); | |
| 55 | |
| 56 this._popoverHelper = new UI.PopoverHelper(this._cursorArea); | |
| 57 this._popoverHelper.initializeCallbacks( | |
| 58 this._getPopoverAnchor.bind(this), this._showPopover.bind(this), this._o
nHidePopover.bind(this)); | |
| 59 this._popoverHelper.setTimeout(0); | |
| 60 | |
| 61 this._updateThrottler = new Common.Throttler(100); | |
| 62 | |
| 63 this._cursorEnabled = false; | |
| 64 this._cursorPosition = 0; | |
| 65 this._lastWidth = 0; | |
| 66 } | |
| 67 | |
| 68 /** | |
| 69 * @param {!Element} element | |
| 70 * @param {!Event} event | |
| 71 * @return {!Element|!AnchorBox|undefined} | |
| 72 */ | |
| 73 _getPopoverAnchor(element, event) { | |
| 74 return this._cursorArea; | |
| 75 } | |
| 76 | |
| 77 /** | |
| 78 * @param {!Element} anchor | |
| 79 * @param {!UI.Popover} popover | |
| 80 */ | |
| 81 _showPopover(anchor, popover) { | |
| 82 this._buildPopoverContents().then(maybeShowPopover.bind(this)); | |
| 83 /** | |
| 84 * @this {UI.TimelineOverviewPane} | |
| 85 * @param {!DocumentFragment} fragment | |
| 86 */ | |
| 87 function maybeShowPopover(fragment) { | |
| 88 if (!fragment.firstChild) | |
| 89 return; | |
| 90 var content = new UI.TimelineOverviewPane.PopoverContents(); | |
| 91 this._popoverContents = content.contentElement.createChild('div'); | |
| 92 this._popoverContents.appendChild(fragment); | |
| 93 this._popover = popover; | |
| 94 popover.showView(content, this._cursorElement); | |
| 95 } | |
| 96 } | |
| 97 | |
| 98 _onHidePopover() { | |
| 99 this._popover = null; | |
| 100 this._popoverContents = null; | |
| 101 } | |
| 102 | |
| 103 /** | |
| 104 * @param {!Event} event | |
| 105 */ | |
| 106 _onMouseMove(event) { | |
| 107 if (!this._cursorEnabled) | |
| 108 return; | |
| 109 this._cursorPosition = event.offsetX + event.target.offsetLeft; | |
| 110 this._cursorElement.style.left = this._cursorPosition + 'px'; | |
| 111 this._cursorElement.style.visibility = 'visible'; | |
| 112 if (!this._popover) | |
| 113 return; | |
| 114 this._buildPopoverContents().then(updatePopover.bind(this)); | |
| 115 this._popover.positionElement(this._cursorElement); | |
| 116 | |
| 117 /** | |
| 118 * @param {!DocumentFragment} fragment | |
| 119 * @this {UI.TimelineOverviewPane} | |
| 120 */ | |
| 121 function updatePopover(fragment) { | |
| 122 if (!this._popoverContents) | |
| 123 return; | |
| 124 this._popoverContents.removeChildren(); | |
| 125 this._popoverContents.appendChild(fragment); | |
| 126 } | |
| 127 } | |
| 128 | |
| 129 /** | |
| 130 * @return {!Promise<!DocumentFragment>} | |
| 131 */ | |
| 132 _buildPopoverContents() { | |
| 133 var document = this.element.ownerDocument; | |
| 134 var x = this._cursorPosition; | |
| 135 var promises = this._overviewControls.map(control => control.popoverElementP
romise(x)); | |
| 136 return Promise.all(promises).then(buildFragment); | |
| 137 | |
| 138 /** | |
| 139 * @param {!Array<?Element>} elements | |
| 140 * @return {!DocumentFragment} | |
| 141 */ | |
| 142 function buildFragment(elements) { | |
| 143 var fragment = document.createDocumentFragment(); | |
| 144 elements.remove(null); | |
| 145 fragment.appendChildren.apply(fragment, elements); | |
| 146 return fragment; | |
| 147 } | |
| 148 } | |
| 149 | |
| 150 _hideCursor() { | |
| 151 this._cursorElement.style.visibility = 'hidden'; | |
| 152 } | |
| 153 | |
| 154 /** | |
| 155 * @override | |
| 156 */ | |
| 157 wasShown() { | |
| 158 this._update(); | |
| 159 } | |
| 160 | |
| 161 /** | |
| 162 * @override | |
| 163 */ | |
| 164 willHide() { | |
| 165 this._popoverHelper.hidePopover(); | |
| 166 } | |
| 167 | |
| 168 /** | |
| 169 * @override | |
| 170 */ | |
| 171 onResize() { | |
| 172 var width = this.element.offsetWidth; | |
| 173 if (width === this._lastWidth) | |
| 174 return; | |
| 175 this._lastWidth = width; | |
| 176 this.scheduleUpdate(); | |
| 177 } | |
| 178 | |
| 179 /** | |
| 180 * @param {!Array.<!UI.TimelineOverview>} overviewControls | |
| 181 */ | |
| 182 setOverviewControls(overviewControls) { | |
| 183 for (var i = 0; i < this._overviewControls.length; ++i) | |
| 184 this._overviewControls[i].dispose(); | |
| 185 | |
| 186 for (var i = 0; i < overviewControls.length; ++i) { | |
| 187 overviewControls[i].setCalculator(this._overviewCalculator); | |
| 188 overviewControls[i].show(this._overviewGrid.element); | |
| 189 } | |
| 190 this._overviewControls = overviewControls; | |
| 191 this._update(); | |
| 192 } | |
| 193 | |
| 194 /** | |
| 195 * @param {number} minimumBoundary | |
| 196 * @param {number} maximumBoundary | |
| 197 */ | |
| 198 setBounds(minimumBoundary, maximumBoundary) { | |
| 199 this._overviewCalculator.setBounds(minimumBoundary, maximumBoundary); | |
| 200 this._overviewGrid.setResizeEnabled(true); | |
| 201 this._cursorEnabled = true; | |
| 202 } | |
| 203 | |
| 204 scheduleUpdate() { | |
| 205 this._updateThrottler.schedule(process.bind(this)); | |
| 206 /** | |
| 207 * @this {UI.TimelineOverviewPane} | |
| 208 * @return {!Promise.<undefined>} | |
| 209 */ | |
| 210 function process() { | |
| 211 this._update(); | |
| 212 return Promise.resolve(); | |
| 213 } | |
| 214 } | |
| 215 | |
| 216 _update() { | |
| 217 if (!this.isShowing()) | |
| 218 return; | |
| 219 this._overviewCalculator.setDisplayWindow(this._overviewGrid.clientWidth()); | |
| 220 for (var i = 0; i < this._overviewControls.length; ++i) | |
| 221 this._overviewControls[i].update(); | |
| 222 this._overviewGrid.updateDividers(this._overviewCalculator); | |
| 223 this._updateMarkers(); | |
| 224 this._updateWindow(); | |
| 225 } | |
| 226 | |
| 227 /** | |
| 228 * @param {!Map<number, !Element>} markers | |
| 229 */ | |
| 230 setMarkers(markers) { | |
| 231 this._markers = markers; | |
| 232 this._updateMarkers(); | |
| 233 } | |
| 234 | |
| 235 _updateMarkers() { | |
| 236 var filteredMarkers = new Map(); | |
| 237 for (var time of this._markers.keys()) { | |
| 238 var marker = this._markers.get(time); | |
| 239 var position = Math.round(this._overviewCalculator.computePosition(time)); | |
| 240 // Limit the number of markers to one per pixel. | |
| 241 if (filteredMarkers.has(position)) | |
| 242 continue; | |
| 243 filteredMarkers.set(position, marker); | |
| 244 marker.style.left = position + 'px'; | |
| 245 } | |
| 246 this._overviewGrid.removeEventDividers(); | |
| 247 this._overviewGrid.addEventDividers(filteredMarkers.valuesArray()); | |
| 248 } | |
| 249 | |
| 250 reset() { | |
| 251 this._windowStartTime = 0; | |
| 252 this._windowEndTime = Infinity; | |
| 253 this._overviewCalculator.reset(); | |
| 254 this._overviewGrid.reset(); | |
| 255 this._overviewGrid.setResizeEnabled(false); | |
| 256 this._overviewGrid.updateDividers(this._overviewCalculator); | |
| 257 this._cursorEnabled = false; | |
| 258 this._hideCursor(); | |
| 259 this._markers = new Map(); | |
| 260 for (var i = 0; i < this._overviewControls.length; ++i) | |
| 261 this._overviewControls[i].reset(); | |
| 262 this._popoverHelper.hidePopover(); | |
| 263 this._update(); | |
| 264 } | |
| 265 | |
| 266 /** | |
| 267 * @param {!Event} event | |
| 268 * @return {boolean} | |
| 269 */ | |
| 270 _onClick(event) { | |
| 271 for (var overviewControl of this._overviewControls) { | |
| 272 if (overviewControl.onClick(event)) | |
| 273 return true; | |
| 274 } | |
| 275 return false; | |
| 276 } | |
| 277 | |
| 278 /** | |
| 279 * @param {!Common.Event} event | |
| 280 */ | |
| 281 _onWindowChanged(event) { | |
| 282 if (this._muteOnWindowChanged) | |
| 283 return; | |
| 284 // Always use first control as a time converter. | |
| 285 if (!this._overviewControls.length) | |
| 286 return; | |
| 287 var windowTimes = | |
| 288 this._overviewControls[0].windowTimes(this._overviewGrid.windowLeft(), t
his._overviewGrid.windowRight()); | |
| 289 this._windowStartTime = windowTimes.startTime; | |
| 290 this._windowEndTime = windowTimes.endTime; | |
| 291 this.dispatchEventToListeners(UI.TimelineOverviewPane.Events.WindowChanged,
windowTimes); | |
| 292 } | |
| 293 | |
| 294 /** | |
| 295 * @param {number} startTime | |
| 296 * @param {number} endTime | |
| 297 */ | |
| 298 requestWindowTimes(startTime, endTime) { | |
| 299 if (startTime === this._windowStartTime && endTime === this._windowEndTime) | |
| 300 return; | |
| 301 this._windowStartTime = startTime; | |
| 302 this._windowEndTime = endTime; | |
| 303 this._updateWindow(); | |
| 304 this.dispatchEventToListeners( | |
| 305 UI.TimelineOverviewPane.Events.WindowChanged, {startTime: startTime, end
Time: endTime}); | |
| 306 } | |
| 307 | |
| 308 _updateWindow() { | |
| 309 if (!this._overviewControls.length) | |
| 310 return; | |
| 311 var windowBoundaries = this._overviewControls[0].windowBoundaries(this._wind
owStartTime, this._windowEndTime); | |
| 312 this._muteOnWindowChanged = true; | |
| 313 this._overviewGrid.setWindow(windowBoundaries.left, windowBoundaries.right); | |
| 314 this._muteOnWindowChanged = false; | |
| 315 } | |
| 316 }; | |
| 317 | |
| 318 /** @enum {symbol} */ | |
| 319 UI.TimelineOverviewPane.Events = { | |
| 320 WindowChanged: Symbol('WindowChanged') | |
| 321 }; | |
| 322 | |
| 323 /** | |
| 324 * @unrestricted | |
| 325 */ | |
| 326 UI.TimelineOverviewPane.PopoverContents = class extends UI.VBox { | |
| 327 constructor() { | |
| 328 super(true); | |
| 329 this.contentElement.classList.add('timeline-overview-popover'); | |
| 330 } | |
| 331 }; | |
| 332 | |
| 333 /** | |
| 334 * @implements {UI.TimelineGrid.Calculator} | |
| 335 * @unrestricted | |
| 336 */ | |
| 337 UI.TimelineOverviewCalculator = class { | |
| 338 constructor() { | |
| 339 this.reset(); | |
| 340 } | |
| 341 | |
| 342 /** | |
| 343 * @override | |
| 344 * @return {number} | |
| 345 */ | |
| 346 paddingLeft() { | |
| 347 return this._paddingLeft; | |
| 348 } | |
| 349 | |
| 350 /** | |
| 351 * @override | |
| 352 * @param {number} time | |
| 353 * @return {number} | |
| 354 */ | |
| 355 computePosition(time) { | |
| 356 return (time - this._minimumBoundary) / this.boundarySpan() * this._workingA
rea + this._paddingLeft; | |
| 357 } | |
| 358 | |
| 359 /** | |
| 360 * @param {number} position | |
| 361 * @return {number} | |
| 362 */ | |
| 363 positionToTime(position) { | |
| 364 return (position - this._paddingLeft) / this._workingArea * this.boundarySpa
n() + this._minimumBoundary; | |
| 365 } | |
| 366 | |
| 367 /** | |
| 368 * @param {number} minimumBoundary | |
| 369 * @param {number} maximumBoundary | |
| 370 */ | |
| 371 setBounds(minimumBoundary, maximumBoundary) { | |
| 372 this._minimumBoundary = minimumBoundary; | |
| 373 this._maximumBoundary = maximumBoundary; | |
| 374 } | |
| 375 | |
| 376 /** | |
| 377 * @param {number} clientWidth | |
| 378 * @param {number=} paddingLeft | |
| 379 */ | |
| 380 setDisplayWindow(clientWidth, paddingLeft) { | |
| 381 this._paddingLeft = paddingLeft || 0; | |
| 382 this._workingArea = clientWidth - this._paddingLeft; | |
| 383 } | |
| 384 | |
| 385 reset() { | |
| 386 this.setBounds(0, 100); | |
| 387 } | |
| 388 | |
| 389 /** | |
| 390 * @override | |
| 391 * @param {number} value | |
| 392 * @param {number=} precision | |
| 393 * @return {string} | |
| 394 */ | |
| 395 formatValue(value, precision) { | |
| 396 return Number.preciseMillisToString(value - this.zeroTime(), precision); | |
| 397 } | |
| 398 | |
| 399 /** | |
| 400 * @override | |
| 401 * @return {number} | |
| 402 */ | |
| 403 maximumBoundary() { | |
| 404 return this._maximumBoundary; | |
| 405 } | |
| 406 | |
| 407 /** | |
| 408 * @override | |
| 409 * @return {number} | |
| 410 */ | |
| 411 minimumBoundary() { | |
| 412 return this._minimumBoundary; | |
| 413 } | |
| 414 | |
| 415 /** | |
| 416 * @override | |
| 417 * @return {number} | |
| 418 */ | |
| 419 zeroTime() { | |
| 420 return this._minimumBoundary; | |
| 421 } | |
| 422 | |
| 423 /** | |
| 424 * @override | |
| 425 * @return {number} | |
| 426 */ | |
| 427 boundarySpan() { | |
| 428 return this._maximumBoundary - this._minimumBoundary; | |
| 429 } | |
| 430 }; | |
| 431 | |
| 432 /** | |
| 433 * @interface | |
| 434 */ | |
| 435 UI.TimelineOverview = function() {}; | |
| 436 | |
| 437 UI.TimelineOverview.prototype = { | |
| 438 /** | |
| 439 * @param {!Element} parentElement | |
| 440 * @param {?Element=} insertBefore | |
| 441 */ | |
| 442 show(parentElement, insertBefore) {}, | |
| 443 | |
| 444 update() {}, | |
| 445 | |
| 446 dispose() {}, | |
| 447 | |
| 448 reset() {}, | |
| 449 | |
| 450 /** | |
| 451 * @param {number} x | |
| 452 * @return {!Promise<?Element>} | |
| 453 */ | |
| 454 popoverElementPromise(x) {}, | |
| 455 | |
| 456 /** | |
| 457 * @param {!Event} event | |
| 458 * @return {boolean} | |
| 459 */ | |
| 460 onClick(event) {}, | |
| 461 | |
| 462 /** | |
| 463 * @param {number} windowLeft | |
| 464 * @param {number} windowRight | |
| 465 * @return {!{startTime: number, endTime: number}} | |
| 466 */ | |
| 467 windowTimes(windowLeft, windowRight) {}, | |
| 468 | |
| 469 /** | |
| 470 * @param {number} startTime | |
| 471 * @param {number} endTime | |
| 472 * @return {!{left: number, right: number}} | |
| 473 */ | |
| 474 windowBoundaries(startTime, endTime) {}, | |
| 475 | |
| 476 timelineStarted() {}, | |
| 477 | |
| 478 timelineStopped() {}, | |
| 479 }; | |
| 480 | |
| 481 /** | |
| 482 * @implements {UI.TimelineOverview} | |
| 483 * @unrestricted | |
| 484 */ | |
| 485 UI.TimelineOverviewBase = class extends UI.VBox { | |
| 486 constructor() { | |
| 487 super(); | |
| 488 /** @type {?UI.TimelineOverviewCalculator} */ | |
| 489 this._calculator = null; | |
| 490 this._canvas = this.element.createChild('canvas', 'fill'); | |
| 491 this._context = this._canvas.getContext('2d'); | |
| 492 } | |
| 493 | |
| 494 /** @return {number} */ | |
| 495 width() { | |
| 496 return this._canvas.width; | |
| 497 } | |
| 498 | |
| 499 /** @return {number} */ | |
| 500 height() { | |
| 501 return this._canvas.height; | |
| 502 } | |
| 503 | |
| 504 /** @return {!CanvasRenderingContext2D} */ | |
| 505 context() { | |
| 506 return this._context; | |
| 507 } | |
| 508 | |
| 509 /** | |
| 510 * @protected | |
| 511 * @return {?UI.TimelineOverviewCalculator} | |
| 512 */ | |
| 513 calculator() { | |
| 514 return this._calculator; | |
| 515 } | |
| 516 | |
| 517 /** | |
| 518 * @override | |
| 519 */ | |
| 520 update() { | |
| 521 this.resetCanvas(); | |
| 522 } | |
| 523 | |
| 524 /** | |
| 525 * @override | |
| 526 */ | |
| 527 dispose() { | |
| 528 this.detach(); | |
| 529 } | |
| 530 | |
| 531 /** | |
| 532 * @override | |
| 533 */ | |
| 534 reset() { | |
| 535 } | |
| 536 | |
| 537 /** | |
| 538 * @override | |
| 539 * @param {number} x | |
| 540 * @return {!Promise<?Element>} | |
| 541 */ | |
| 542 popoverElementPromise(x) { | |
| 543 return Promise.resolve(/** @type {?Element} */ (null)); | |
| 544 } | |
| 545 | |
| 546 /** | |
| 547 * @override | |
| 548 */ | |
| 549 timelineStarted() { | |
| 550 } | |
| 551 | |
| 552 /** | |
| 553 * @override | |
| 554 */ | |
| 555 timelineStopped() { | |
| 556 } | |
| 557 | |
| 558 /** | |
| 559 * @param {!UI.TimelineOverviewCalculator} calculator | |
| 560 */ | |
| 561 setCalculator(calculator) { | |
| 562 this._calculator = calculator; | |
| 563 } | |
| 564 | |
| 565 /** | |
| 566 * @override | |
| 567 * @param {!Event} event | |
| 568 * @return {boolean} | |
| 569 */ | |
| 570 onClick(event) { | |
| 571 return false; | |
| 572 } | |
| 573 | |
| 574 /** | |
| 575 * @override | |
| 576 * @param {number} windowLeft | |
| 577 * @param {number} windowRight | |
| 578 * @return {!{startTime: number, endTime: number}} | |
| 579 */ | |
| 580 windowTimes(windowLeft, windowRight) { | |
| 581 var absoluteMin = this._calculator.minimumBoundary(); | |
| 582 var timeSpan = this._calculator.maximumBoundary() - absoluteMin; | |
| 583 return {startTime: absoluteMin + timeSpan * windowLeft, endTime: absoluteMin
+ timeSpan * windowRight}; | |
| 584 } | |
| 585 | |
| 586 /** | |
| 587 * @override | |
| 588 * @param {number} startTime | |
| 589 * @param {number} endTime | |
| 590 * @return {!{left: number, right: number}} | |
| 591 */ | |
| 592 windowBoundaries(startTime, endTime) { | |
| 593 var absoluteMin = this._calculator.minimumBoundary(); | |
| 594 var timeSpan = this._calculator.maximumBoundary() - absoluteMin; | |
| 595 var haveRecords = absoluteMin > 0; | |
| 596 return { | |
| 597 left: haveRecords && startTime ? Math.min((startTime - absoluteMin) / time
Span, 1) : 0, | |
| 598 right: haveRecords && endTime < Infinity ? (endTime - absoluteMin) / timeS
pan : 1 | |
| 599 }; | |
| 600 } | |
| 601 | |
| 602 resetCanvas() { | |
| 603 this._canvas.width = this.element.clientWidth * window.devicePixelRatio; | |
| 604 this._canvas.height = this.element.clientHeight * window.devicePixelRatio; | |
| 605 } | |
| 606 }; | |
| OLD | NEW |