OLD | NEW |
| (Empty) |
1 // Copyright 2016 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 * @unrestricted | |
6 */ | |
7 UI.ChartViewport = class extends UI.VBox { | |
8 constructor() { | |
9 super(true); | |
10 | |
11 this.viewportElement = this.contentElement.createChild('div', 'fill'); | |
12 this.viewportElement.addEventListener('mousewheel', this._onMouseWheel.bind(
this), false); | |
13 this.viewportElement.addEventListener('keydown', this._handleZoomPanKeys.bin
d(this), false); | |
14 | |
15 UI.installInertialDragHandle( | |
16 this.viewportElement, this._startDragging.bind(this), this._dragging.bin
d(this), this._endDragging.bind(this), | |
17 '-webkit-grabbing', null); | |
18 UI.installDragHandle( | |
19 this.viewportElement, this._startRangeSelection.bind(this), this._rangeS
electionDragging.bind(this), | |
20 this._endRangeSelection.bind(this), 'text', null); | |
21 | |
22 this._alwaysShowVerticalScroll = false; | |
23 this._vScrollElement = this.contentElement.createChild('div', 'flame-chart-v
-scroll'); | |
24 this._vScrollContent = this._vScrollElement.createChild('div'); | |
25 this._vScrollElement.addEventListener('scroll', this._onScroll.bind(this), f
alse); | |
26 | |
27 this._selectionOverlay = this.contentElement.createChild('div', 'flame-chart
-selection-overlay hidden'); | |
28 this._selectedTimeSpanLabel = this._selectionOverlay.createChild('div', 'tim
e-span'); | |
29 | |
30 this.reset(); | |
31 } | |
32 | |
33 alwaysShowVerticalScroll() { | |
34 this._alwaysShowVerticalScroll = true; | |
35 this._vScrollElement.classList.add('always-show-scrollbar'); | |
36 } | |
37 | |
38 /** | |
39 * @return {boolean} | |
40 */ | |
41 isDragging() { | |
42 return this._isDragging; | |
43 } | |
44 | |
45 /** | |
46 * @override | |
47 * @return {!Array<!Element>} | |
48 */ | |
49 elementsToRestoreScrollPositionsFor() { | |
50 return [this._vScrollElement]; | |
51 } | |
52 | |
53 /** | |
54 * @private | |
55 */ | |
56 _updateScrollBar() { | |
57 const showScroll = this._alwaysShowVerticalScroll || this._totalHeight > thi
s._offsetHeight; | |
58 if (this._vScrollElement.classList.contains('hidden') !== showScroll) | |
59 return; | |
60 this._vScrollElement.classList.toggle('hidden', !showScroll); | |
61 this._updateContentElementSize(); | |
62 } | |
63 | |
64 /** | |
65 * @override | |
66 */ | |
67 onResize() { | |
68 this._updateScrollBar(); | |
69 this._updateContentElementSize(); | |
70 this.scheduleUpdate(); | |
71 } | |
72 | |
73 reset() { | |
74 this._vScrollElement.scrollTop = 0; | |
75 this._scrollTop = 0; | |
76 this._rangeSelectionStart = 0; | |
77 this._rangeSelectionEnd = 0; | |
78 this._isDragging = false; | |
79 this._dragStartPointX = 0; | |
80 this._dragStartPointY = 0; | |
81 this._dragStartScrollTop = 0; | |
82 this._timeWindowLeft = 0; | |
83 this._timeWindowRight = 0; | |
84 this._offsetWidth = 0; | |
85 this._offsetHeight = 0; | |
86 this._totalHeight = 0; | |
87 this._pendingAnimationTimeLeft = 0; | |
88 this._pendingAnimationTimeRight = 0; | |
89 } | |
90 | |
91 /** | |
92 * @private | |
93 */ | |
94 _updateContentElementSize() { | |
95 var offsetWidth = this._vScrollElement.offsetLeft; | |
96 if (!offsetWidth) | |
97 offsetWidth = this.contentElement.offsetWidth; | |
98 this._offsetWidth = offsetWidth; | |
99 this._offsetHeight = this.contentElement.offsetHeight; | |
100 } | |
101 | |
102 setContentHeight(totalHeight) { | |
103 this._totalHeight = totalHeight; | |
104 this._vScrollContent.style.height = totalHeight + 'px'; | |
105 this._updateScrollBar(); | |
106 if (this._scrollTop + this._offsetHeight <= totalHeight) | |
107 return; | |
108 this._scrollTop = Math.max(0, totalHeight - this._offsetHeight); | |
109 this._vScrollElement.scrollTop = this._scrollTop; | |
110 } | |
111 | |
112 /** | |
113 * @param {number} offset | |
114 * @param {number=} height | |
115 */ | |
116 setScrollOffset(offset, height) { | |
117 height = height || 0; | |
118 if (this._vScrollElement.scrollTop > offset) | |
119 this._vScrollElement.scrollTop = offset; | |
120 else if (this._vScrollElement.scrollTop < offset - this._offsetHeight + heig
ht) | |
121 this._vScrollElement.scrollTop = offset - this._offsetHeight + height; | |
122 } | |
123 | |
124 /** | |
125 * @return {number} | |
126 */ | |
127 getScrollOffset() { | |
128 return this._vScrollElement.scrollTop; | |
129 } | |
130 | |
131 /** | |
132 * @param {!Event} e | |
133 * @private | |
134 */ | |
135 _onMouseWheel(e) { | |
136 if (!this._enabled()) | |
137 return; | |
138 // Pan vertically when shift down only. | |
139 var panVertically = e.shiftKey && (e.wheelDeltaY || Math.abs(e.wheelDeltaX)
=== 120); | |
140 var panHorizontally = Math.abs(e.wheelDeltaX) > Math.abs(e.wheelDeltaY) && !
e.shiftKey; | |
141 if (panVertically) { | |
142 this._vScrollElement.scrollTop -= (e.wheelDeltaY || e.wheelDeltaX) / 120 *
this._offsetHeight / 8; | |
143 } else if (panHorizontally) { | |
144 var shift = -e.wheelDeltaX * this._pixelToTime; | |
145 this._muteAnimation = true; | |
146 this._handlePanGesture(shift); | |
147 this._muteAnimation = false; | |
148 } else { // Zoom. | |
149 const mouseWheelZoomSpeed = 1 / 120; | |
150 this._handleZoomGesture(Math.pow(1.2, -(e.wheelDeltaY || e.wheelDeltaX) *
mouseWheelZoomSpeed) - 1); | |
151 } | |
152 | |
153 // Block swipe gesture. | |
154 e.consume(true); | |
155 } | |
156 | |
157 /** | |
158 * @param {number} x | |
159 * @param {number} y | |
160 * @param {!MouseEvent} event | |
161 * @private | |
162 * @return {boolean} | |
163 */ | |
164 _startDragging(x, y, event) { | |
165 if (event.shiftKey) | |
166 return false; | |
167 if (this._windowRight === Infinity) | |
168 return false; | |
169 this._isDragging = true; | |
170 this._initMaxDragOffset(event); | |
171 this._dragStartPointX = x; | |
172 this._dragStartPointY = y; | |
173 this._dragStartScrollTop = this._vScrollElement.scrollTop; | |
174 this.viewportElement.style.cursor = ''; | |
175 this.hideHighlight(); | |
176 return true; | |
177 } | |
178 | |
179 /** | |
180 * @param {number} x | |
181 * @param {number} y | |
182 * @private | |
183 */ | |
184 _dragging(x, y) { | |
185 var pixelShift = this._dragStartPointX - x; | |
186 this._dragStartPointX = x; | |
187 this._muteAnimation = true; | |
188 this._handlePanGesture(pixelShift * this._pixelToTime); | |
189 this._muteAnimation = false; | |
190 | |
191 var pixelScroll = this._dragStartPointY - y; | |
192 this._vScrollElement.scrollTop = this._dragStartScrollTop + pixelScroll; | |
193 this._updateMaxDragOffset(x, y); | |
194 } | |
195 | |
196 /** | |
197 * @private | |
198 */ | |
199 _endDragging() { | |
200 this._isDragging = false; | |
201 this._updateHighlight(); | |
202 } | |
203 | |
204 /** | |
205 * @param {!MouseEvent} event | |
206 * @private | |
207 */ | |
208 _initMaxDragOffset(event) { | |
209 this._maxDragOffsetSquared = 0; | |
210 this._dragStartX = event.pageX; | |
211 this._dragStartY = event.pageY; | |
212 } | |
213 | |
214 /** | |
215 * @param {number} x | |
216 * @param {number} y | |
217 * @private | |
218 */ | |
219 _updateMaxDragOffset(x, y) { | |
220 var dx = x - this._dragStartX; | |
221 var dy = y - this._dragStartY; | |
222 var dragOffsetSquared = dx * dx + dy * dy; | |
223 this._maxDragOffsetSquared = Math.max(this._maxDragOffsetSquared, dragOffset
Squared); | |
224 } | |
225 | |
226 /** | |
227 * @return {number} | |
228 */ | |
229 maxDragOffset() { | |
230 return Math.sqrt(this._maxDragOffsetSquared); | |
231 } | |
232 | |
233 /** | |
234 * @param {!MouseEvent} event | |
235 * @private | |
236 * @return {boolean} | |
237 */ | |
238 _startRangeSelection(event) { | |
239 if (!event.shiftKey) | |
240 return false; | |
241 this._isDragging = true; | |
242 this._initMaxDragOffset(event); | |
243 this._selectionOffsetShiftX = event.offsetX - event.pageX; | |
244 this._selectionOffsetShiftY = event.offsetY - event.pageY; | |
245 this._selectionStartX = event.offsetX; | |
246 var style = this._selectionOverlay.style; | |
247 style.left = this._selectionStartX + 'px'; | |
248 style.width = '1px'; | |
249 this._selectedTimeSpanLabel.textContent = ''; | |
250 this._selectionOverlay.classList.remove('hidden'); | |
251 this.hideHighlight(); | |
252 return true; | |
253 } | |
254 | |
255 /** | |
256 * @private | |
257 */ | |
258 _endRangeSelection() { | |
259 this._isDragging = false; | |
260 this._updateHighlight(); | |
261 } | |
262 | |
263 hideRangeSelection() { | |
264 this._selectionOverlay.classList.add('hidden'); | |
265 } | |
266 | |
267 /** | |
268 * @param {!MouseEvent} event | |
269 * @private | |
270 */ | |
271 _rangeSelectionDragging(event) { | |
272 this._updateMaxDragOffset(event.pageX, event.pageY); | |
273 var x = Number.constrain(event.pageX + this._selectionOffsetShiftX, 0, this.
_offsetWidth); | |
274 var start = this._cursorTime(this._selectionStartX); | |
275 var end = this._cursorTime(x); | |
276 this._rangeSelectionStart = Math.min(start, end); | |
277 this._rangeSelectionEnd = Math.max(start, end); | |
278 this._updateRangeSelectionOverlay(); | |
279 this._flameChartDelegate.updateRangeSelection(this._rangeSelectionStart, thi
s._rangeSelectionEnd); | |
280 } | |
281 | |
282 /** | |
283 * @private | |
284 */ | |
285 _updateRangeSelectionOverlay() { | |
286 var /** @const */ margin = 100; | |
287 var left = Number.constrain(this._timeToPosition(this._rangeSelectionStart),
-margin, this._offsetWidth + margin); | |
288 var right = Number.constrain(this._timeToPosition(this._rangeSelectionEnd),
-margin, this._offsetWidth + margin); | |
289 var style = this._selectionOverlay.style; | |
290 style.left = left + 'px'; | |
291 style.width = (right - left) + 'px'; | |
292 var timeSpan = this._rangeSelectionEnd - this._rangeSelectionStart; | |
293 this._selectedTimeSpanLabel.textContent = Number.preciseMillisToString(timeS
pan, 2); | |
294 } | |
295 | |
296 /** | |
297 * @private | |
298 */ | |
299 _onScroll() { | |
300 this._scrollTop = this._vScrollElement.scrollTop; | |
301 this.scheduleUpdate(); | |
302 } | |
303 | |
304 /** | |
305 * @param {!Event} e | |
306 * @private | |
307 */ | |
308 _handleZoomPanKeys(e) { | |
309 if (!UI.KeyboardShortcut.hasNoModifiers(e)) | |
310 return; | |
311 var zoomMultiplier = e.shiftKey ? 0.8 : 0.3; | |
312 var panMultiplier = e.shiftKey ? 320 : 80; | |
313 if (e.code === 'KeyA') { | |
314 this._handlePanGesture(-panMultiplier * this._pixelToTime); | |
315 e.consume(true); | |
316 } else if (e.code === 'KeyD') { | |
317 this._handlePanGesture(panMultiplier * this._pixelToTime); | |
318 e.consume(true); | |
319 } else if (e.code === 'KeyW') { | |
320 this._handleZoomGesture(-zoomMultiplier); | |
321 e.consume(true); | |
322 } else if (e.code === 'KeyS') { | |
323 this._handleZoomGesture(zoomMultiplier); | |
324 e.consume(true); | |
325 } | |
326 } | |
327 | |
328 /** | |
329 * @param {number} zoom | |
330 * @private | |
331 */ | |
332 _handleZoomGesture(zoom) { | |
333 this._cancelAnimation(); | |
334 var bounds = this._windowForGesture(); | |
335 var cursorTime = this._cursorTime(this._lastMouseOffsetX); | |
336 bounds.left += (bounds.left - cursorTime) * zoom; | |
337 bounds.right += (bounds.right - cursorTime) * zoom; | |
338 this._requestWindowTimes(bounds); | |
339 } | |
340 | |
341 /** | |
342 * @param {number} shift | |
343 * @private | |
344 */ | |
345 _handlePanGesture(shift) { | |
346 this._cancelAnimation(); | |
347 var bounds = this._windowForGesture(); | |
348 shift = Number.constrain( | |
349 shift, this._minimumBoundary - bounds.left, this._totalTime + this._mini
mumBoundary - bounds.right); | |
350 bounds.left += shift; | |
351 bounds.right += shift; | |
352 this._requestWindowTimes(bounds); | |
353 } | |
354 | |
355 /** | |
356 * @private | |
357 * @return {{left: number, right: number}} | |
358 */ | |
359 _windowForGesture() { | |
360 var windowLeft = this._timeWindowLeft ? this._timeWindowLeft : this._dataPro
vider.minimumBoundary(); | |
361 var windowRight = this._timeWindowRight !== Infinity ? | |
362 this._timeWindowRight : | |
363 this._dataProvider.minimumBoundary() + this._dataProvider.totalTime(); | |
364 return {left: windowLeft, right: windowRight}; | |
365 } | |
366 | |
367 /** | |
368 * @param {{left: number, right: number}} bounds | |
369 * @private | |
370 */ | |
371 _requestWindowTimes(bounds) { | |
372 bounds.left = Number.constrain(bounds.left, this._minimumBoundary, this._tot
alTime + this._minimumBoundary); | |
373 bounds.right = Number.constrain(bounds.right, this._minimumBoundary, this._t
otalTime + this._minimumBoundary); | |
374 if (bounds.right - bounds.left < UI.FlameChart.MinimalTimeWindowMs) | |
375 return; | |
376 this._flameChartDelegate.requestWindowTimes(bounds.left, bounds.right); | |
377 } | |
378 | |
379 /** | |
380 * @param {number} startTime | |
381 * @param {number} endTime | |
382 * @private | |
383 */ | |
384 _animateWindowTimes(startTime, endTime) { | |
385 this._timeWindowLeft = startTime; | |
386 this._timeWindowRight = endTime; | |
387 this._updateHighlight(); | |
388 this.update(); | |
389 } | |
390 | |
391 /** | |
392 * @private | |
393 */ | |
394 _animationCompleted() { | |
395 delete this._cancelWindowTimesAnimation; | |
396 this._updateHighlight(); | |
397 } | |
398 | |
399 /** | |
400 * @private | |
401 */ | |
402 _cancelAnimation() { | |
403 if (!this._cancelWindowTimesAnimation) | |
404 return; | |
405 this._timeWindowLeft = this._pendingAnimationTimeLeft; | |
406 this._timeWindowRight = this._pendingAnimationTimeRight; | |
407 this._cancelWindowTimesAnimation(); | |
408 delete this._cancelWindowTimesAnimation; | |
409 } | |
410 | |
411 scheduleUpdate() { | |
412 if (this._updateTimerId || this._cancelWindowTimesAnimation) | |
413 return; | |
414 this._updateTimerId = this.element.window().requestAnimationFrame(() => { | |
415 this._updateTimerId = 0; | |
416 this.update(); | |
417 }); | |
418 } | |
419 | |
420 update() { | |
421 } | |
422 | |
423 /** | |
424 * @param {number} startTime | |
425 * @param {number} endTime | |
426 */ | |
427 setWindowTimes(startTime, endTime) { | |
428 this.hideRangeSelection(); | |
429 if (this._muteAnimation || this._timeWindowLeft === 0 || this._timeWindowRig
ht === Infinity || | |
430 (startTime === 0 && endTime === Infinity) || (startTime === Infinity &&
endTime === Infinity)) { | |
431 // Initial setup. | |
432 this._timeWindowLeft = startTime; | |
433 this._timeWindowRight = endTime; | |
434 this.scheduleUpdate(); | |
435 return; | |
436 } | |
437 this._cancelAnimation(); | |
438 this._cancelWindowTimesAnimation = UI.animateFunction( | |
439 this.element.window(), this._animateWindowTimes.bind(this), | |
440 [{from: this._timeWindowLeft, to: startTime}, {from: this._timeWindowRig
ht, to: endTime}], 5, | |
441 this._animationCompleted.bind(this)); | |
442 this._pendingAnimationTimeLeft = startTime; | |
443 this._pendingAnimationTimeRight = endTime; | |
444 } | |
445 }; | |
OLD | NEW |