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 |