blob: 2c4a8a115197aa26de562607c09619952709a6ef [file] [log] [blame]
/*
* Copyright (C) 2013 Google Inc. All rights reserved.
*
* Redistribution and use in source and binary forms, with or without
* modification, are permitted provided that the following conditions are
* met:
*
* * Redistributions of source code must retain the above copyright
* notice, this list of conditions and the following disclaimer.
* * Redistributions in binary form must reproduce the above
* copyright notice, this list of conditions and the following disclaimer
* in the documentation and/or other materials provided with the
* distribution.
* * Neither the name of Google Inc. nor the names of its
* contributors may be used to endorse or promote products derived from
* this software without specific prior written permission.
*
* THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
* "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
* LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
* A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
* OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
* SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
* LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
* DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
* THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
* (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
* OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
*/
/**
* @unrestricted
*/
PerfUI.OverviewGrid = class {
/**
* @param {string} prefix
* @param {!PerfUI.TimelineGrid.Calculator=} calculator
*/
constructor(prefix, calculator) {
this.element = createElement('div');
this.element.id = prefix + '-overview-container';
this._grid = new PerfUI.TimelineGrid();
this._grid.element.id = prefix + '-overview-grid';
this._grid.setScrollTop(0);
this.element.appendChild(this._grid.element);
this._window = new PerfUI.OverviewGrid.Window(this.element, this._grid.dividersLabelBarElement, calculator);
}
/**
* @return {number}
*/
clientWidth() {
return this.element.clientWidth;
}
/**
* @param {!PerfUI.TimelineGrid.Calculator} calculator
*/
updateDividers(calculator) {
this._grid.updateDividers(calculator);
}
/**
* @param {!Array.<!Element>} dividers
*/
addEventDividers(dividers) {
this._grid.addEventDividers(dividers);
}
removeEventDividers() {
this._grid.removeEventDividers();
}
reset() {
this._window.reset();
}
/**
* @return {number}
*/
windowLeft() {
return this._window.windowLeft;
}
/**
* @return {number}
*/
windowRight() {
return this._window.windowRight;
}
/**
* @param {number} left
* @param {number} right
*/
setWindow(left, right) {
this._window._setWindow(left, right);
}
/**
* @param {symbol} eventType
* @param {function(!Common.Event)} listener
* @param {!Object=} thisObject
* @return {!Common.EventTarget.EventDescriptor}
*/
addEventListener(eventType, listener, thisObject) {
return this._window.addEventListener(eventType, listener, thisObject);
}
/**
* @param {?function(!Event):boolean} clickHandler
*/
setClickHandler(clickHandler) {
this._window.setClickHandler(clickHandler);
}
/**
* @param {number} zoomFactor
* @param {number} referencePoint
*/
zoom(zoomFactor, referencePoint) {
this._window._zoom(zoomFactor, referencePoint);
}
/**
* @param {boolean} enabled
*/
setResizeEnabled(enabled) {
this._window.setEnabled(enabled);
}
};
PerfUI.OverviewGrid.MinSelectableSize = 14;
PerfUI.OverviewGrid.WindowScrollSpeedFactor = .3;
PerfUI.OverviewGrid.ResizerOffset = 3.5; // half pixel because offset values are not rounded but ceiled
PerfUI.OverviewGrid.OffsetFromWindowEnds = 10;
/**
* @unrestricted
*/
PerfUI.OverviewGrid.Window = class extends Common.Object {
/**
* @param {!Element} parentElement
* @param {!Element=} dividersLabelBarElement
* @param {!PerfUI.TimelineGrid.Calculator=} calculator
*/
constructor(parentElement, dividersLabelBarElement, calculator) {
super();
this._parentElement = parentElement;
UI.ARIAUtils.markAsGroup(this._parentElement);
this._calculator = calculator;
UI.ARIAUtils.setAccessibleName(this._parentElement, ls`Overview grid window`);
UI.installDragHandle(
this._parentElement, this._startWindowSelectorDragging.bind(this), this._windowSelectorDragging.bind(this),
this._endWindowSelectorDragging.bind(this), 'text', null);
if (dividersLabelBarElement) {
UI.installDragHandle(
dividersLabelBarElement, this._startWindowDragging.bind(this), this._windowDragging.bind(this), null,
'-webkit-grabbing', '-webkit-grab');
}
this._parentElement.addEventListener('mousewheel', this._onMouseWheel.bind(this), true);
this._parentElement.addEventListener('dblclick', this._resizeWindowMaximum.bind(this), true);
UI.appendStyle(this._parentElement, 'perf_ui/overviewGrid.css');
this._leftResizeElement = parentElement.createChild('div', 'overview-grid-window-resizer');
UI.installDragHandle(
this._leftResizeElement, this._resizerElementStartDragging.bind(this),
this._leftResizeElementDragging.bind(this), null, 'ew-resize');
this._rightResizeElement = parentElement.createChild('div', 'overview-grid-window-resizer');
UI.installDragHandle(
this._rightResizeElement, this._resizerElementStartDragging.bind(this),
this._rightResizeElementDragging.bind(this), null, 'ew-resize');
UI.ARIAUtils.setAccessibleName(this._leftResizeElement, ls`Left Resizer`);
UI.ARIAUtils.markAsSlider(this._leftResizeElement);
this._leftResizeElement.tabIndex = 0;
this._leftResizeElement.addEventListener('keydown', event => this._handleKeyboardResizing(event, false));
UI.ARIAUtils.setAccessibleName(this._rightResizeElement, ls`Right Resizer`);
UI.ARIAUtils.markAsSlider(this._rightResizeElement);
this._rightResizeElement.tabIndex = 0;
this._rightResizeElement.addEventListener('keydown', event => this._handleKeyboardResizing(event, true));
this._rightResizeElement.addEventListener('focus', this._onRightResizeElementFocused.bind(this));
this._leftCurtainElement = parentElement.createChild('div', 'window-curtain-left');
this._rightCurtainElement = parentElement.createChild('div', 'window-curtain-right');
this.reset();
}
_onRightResizeElementFocused() {
// To prevent browser focus from scrolling the element into view and shifting the contents of the strip
this._parentElement.scrollLeft = 0;
}
reset() {
this.windowLeft = 0.0;
this.windowRight = 1.0;
this.setEnabled(true);
this._updateCurtains();
}
/**
* @param {boolean} enabled
*/
setEnabled(enabled) {
this._enabled = enabled;
}
/**
* @param {?function(!Event):boolean} clickHandler
*/
setClickHandler(clickHandler) {
this._clickHandler = clickHandler;
}
/**
* @param {!Event} event
*/
_resizerElementStartDragging(event) {
if (!this._enabled) {
return false;
}
this._resizerParentOffsetLeft = event.pageX - event.offsetX - event.target.offsetLeft;
event.stopPropagation();
return true;
}
/**
* @param {!Event} event
*/
_leftResizeElementDragging(event) {
this._resizeWindowLeft(event.pageX - this._resizerParentOffsetLeft);
event.preventDefault();
}
/**
* @param {!Event} event
*/
_rightResizeElementDragging(event) {
this._resizeWindowRight(event.pageX - this._resizerParentOffsetLeft);
event.preventDefault();
}
/**
* @param {!Event} event
* @param {boolean=} moveRightResizer
*/
_handleKeyboardResizing(event, moveRightResizer) {
let increment = false;
if (event.key === 'ArrowLeft' || event.key === 'ArrowRight') {
if (event.key === 'ArrowRight') {
increment = true;
}
const newPos = this._getNewResizerPosition(event.target.offsetLeft, increment, event.ctrlKey);
if (moveRightResizer) {
this._resizeWindowRight(newPos);
} else {
this._resizeWindowLeft(newPos);
}
event.consume(true);
}
}
/**
* @param {number} offset
* @param {boolean=} increment
* @param {boolean=} ctrlPressed
* @return {number}
*/
_getNewResizerPosition(offset, increment, ctrlPressed) {
let newPos;
// We shift by 10px if the ctrlKey is pressed and 2 otherwise. 1px shifts result in noOp due to rounding in _updateCurtains
let pixelsToShift = ctrlPressed ? 10 : 2;
pixelsToShift = increment ? pixelsToShift : -Math.abs(pixelsToShift);
const offsetLeft = offset + PerfUI.OverviewGrid.ResizerOffset;
newPos = offsetLeft + pixelsToShift;
if (increment && newPos < PerfUI.OverviewGrid.OffsetFromWindowEnds) {
// When incrementing, snap to the window offset value (10px) if the new position is between 0px and 10px
newPos = PerfUI.OverviewGrid.OffsetFromWindowEnds;
} else if (!increment && newPos > this._parentElement.clientWidth - PerfUI.OverviewGrid.OffsetFromWindowEnds) {
// When decrementing, snap to the window offset value (10px) from the rightmost side if the new position is within 10px from the end.
newPos = this._parentElement.clientWidth - PerfUI.OverviewGrid.OffsetFromWindowEnds;
}
return newPos;
}
/**
* @param {!Event} event
* @return {boolean}
*/
_startWindowSelectorDragging(event) {
if (!this._enabled) {
return false;
}
this._offsetLeft = this._parentElement.totalOffsetLeft();
const position = event.x - this._offsetLeft;
this._overviewWindowSelector = new PerfUI.OverviewGrid.WindowSelector(this._parentElement, position);
return true;
}
/**
* @param {!Event} event
*/
_windowSelectorDragging(event) {
this._overviewWindowSelector._updatePosition(event.x - this._offsetLeft);
event.preventDefault();
}
/**
* @param {!Event} event
*/
_endWindowSelectorDragging(event) {
const window = this._overviewWindowSelector._close(event.x - this._offsetLeft);
delete this._overviewWindowSelector;
const clickThreshold = 3;
if (window.end - window.start < clickThreshold) {
if (this._clickHandler && this._clickHandler.call(null, event)) {
return;
}
const middle = window.end;
window.start = Math.max(0, middle - PerfUI.OverviewGrid.MinSelectableSize / 2);
window.end = Math.min(this._parentElement.clientWidth, middle + PerfUI.OverviewGrid.MinSelectableSize / 2);
} else if (window.end - window.start < PerfUI.OverviewGrid.MinSelectableSize) {
if (this._parentElement.clientWidth - window.end > PerfUI.OverviewGrid.MinSelectableSize) {
window.end = window.start + PerfUI.OverviewGrid.MinSelectableSize;
} else {
window.start = window.end - PerfUI.OverviewGrid.MinSelectableSize;
}
}
this._setWindowPosition(window.start, window.end);
}
/**
* @param {!Event} event
* @return {boolean}
*/
_startWindowDragging(event) {
this._dragStartPoint = event.pageX;
this._dragStartLeft = this.windowLeft;
this._dragStartRight = this.windowRight;
event.stopPropagation();
return true;
}
/**
* @param {!Event} event
*/
_windowDragging(event) {
event.preventDefault();
let delta = (event.pageX - this._dragStartPoint) / this._parentElement.clientWidth;
if (this._dragStartLeft + delta < 0) {
delta = -this._dragStartLeft;
}
if (this._dragStartRight + delta > 1) {
delta = 1 - this._dragStartRight;
}
this._setWindow(this._dragStartLeft + delta, this._dragStartRight + delta);
}
/**
* @param {number} start
*/
_resizeWindowLeft(start) {
// Glue to edge.
if (start < PerfUI.OverviewGrid.OffsetFromWindowEnds) {
start = 0;
} else if (start > this._rightResizeElement.offsetLeft - 4) {
start = this._rightResizeElement.offsetLeft - 4;
}
this._setWindowPosition(start, null);
}
/**
* @param {number} end
*/
_resizeWindowRight(end) {
// Glue to edge.
if (end > this._parentElement.clientWidth - PerfUI.OverviewGrid.OffsetFromWindowEnds) {
end = this._parentElement.clientWidth;
} else if (end < this._leftResizeElement.offsetLeft + PerfUI.OverviewGrid.MinSelectableSize) {
end = this._leftResizeElement.offsetLeft + PerfUI.OverviewGrid.MinSelectableSize;
}
this._setWindowPosition(null, end);
}
_resizeWindowMaximum() {
this._setWindowPosition(0, this._parentElement.clientWidth);
}
/**
* @param {boolean=} leftSlider
* @return {number}
*/
_getRawSliderValue(leftSlider) {
const minimumValue = this._calculator.minimumBoundary();
const valueSpan = this._calculator.maximumBoundary() - minimumValue;
if (leftSlider) {
return minimumValue + valueSpan * this.windowLeft;
} else {
return minimumValue + valueSpan * this.windowRight;
}
}
/**
* @param {number} leftValue
* @param {number} rightValue
*/
_updateResizeElementPositionValue(leftValue, rightValue) {
const roundedLeftValue = leftValue.toFixed(2);
const roundedRightValue = rightValue.toFixed(2);
UI.ARIAUtils.setAriaValueNow(this._leftResizeElement, roundedLeftValue);
UI.ARIAUtils.setAriaValueNow(this._rightResizeElement, roundedRightValue);
// Left and right sliders cannot be within 0.5% of each other (Range of AriaValueMin/Max/Now is from 0-100).
const leftResizeCeiling = roundedRightValue - 0.5;
const rightResizeFloor = Number(roundedLeftValue) + 0.5;
UI.ARIAUtils.setAriaValueMinMax(this._leftResizeElement, '0', leftResizeCeiling.toString());
UI.ARIAUtils.setAriaValueMinMax(this._rightResizeElement, rightResizeFloor.toString(), '100');
}
_updateResizeElementPositionLabels() {
const startValue = this._calculator.formatValue(this._getRawSliderValue(/* leftSlider */ true));
const endValue = this._calculator.formatValue(this._getRawSliderValue(/* leftSlider */ false));
UI.ARIAUtils.setAriaValueText(this._leftResizeElement, String(startValue));
UI.ARIAUtils.setAriaValueText(this._rightResizeElement, String(endValue));
}
/**
* @param {string} leftValue
* @param {string} rightValue
*/
_updateResizeElementPercentageLabels(leftValue, rightValue) {
UI.ARIAUtils.setAriaValueText(this._leftResizeElement, leftValue);
UI.ARIAUtils.setAriaValueText(this._rightResizeElement, rightValue);
}
/**
* @return {{rawStartValue: number, rawEndValue: number}}
*/
_calculateWindowPosition() {
return {
rawStartValue: Number(this._getRawSliderValue(/* leftSlider */ true)),
rawEndValue: Number(this._getRawSliderValue(/* leftSlider */ false))
};
}
/**
* @param {number} windowLeft
* @param {number} windowRight
*/
_setWindow(windowLeft, windowRight) {
this.windowLeft = windowLeft;
this.windowRight = windowRight;
this._updateCurtains();
let windowPosition;
if (this._calculator) {
windowPosition = this._calculateWindowPosition();
}
this.dispatchEventToListeners(PerfUI.OverviewGrid.Events.WindowChanged, windowPosition);
}
_updateCurtains() {
let left = this.windowLeft;
let right = this.windowRight;
const width = right - left;
// OverviewGrids that are instantiated before the parentElement is shown will have a parent element client width of 0 which throws off the 'factor' calculation
if (this._parentElement.clientWidth !== 0) {
// We allow actual time window to be arbitrarily small but don't want the UI window to be too small.
const widthInPixels = width * this._parentElement.clientWidth;
const minWidthInPixels = PerfUI.OverviewGrid.MinSelectableSize / 2;
if (widthInPixels < minWidthInPixels) {
const factor = minWidthInPixels / widthInPixels;
left = ((this.windowRight + this.windowLeft) - width * factor) / 2;
right = ((this.windowRight + this.windowLeft) + width * factor) / 2;
}
}
const leftResizerPercLeftOffset = (100 * left);
const rightResizerPercLeftOffset = (100 * right);
const rightResizerPercRightOffset = (100 - (100 * right));
const leftResizerPercLeftOffsetString = leftResizerPercLeftOffset + '%';
const rightResizerPercLeftOffsetString = rightResizerPercLeftOffset + '%';
this._leftResizeElement.style.left = leftResizerPercLeftOffsetString;
this._rightResizeElement.style.left = rightResizerPercLeftOffsetString;
this._leftCurtainElement.style.width = leftResizerPercLeftOffsetString;
this._rightCurtainElement.style.width = rightResizerPercRightOffset + '%';
this._updateResizeElementPositionValue(leftResizerPercLeftOffset, rightResizerPercLeftOffset);
if (this._calculator) {
this._updateResizeElementPositionLabels();
} else {
this._updateResizeElementPercentageLabels(leftResizerPercLeftOffsetString, rightResizerPercLeftOffsetString);
}
}
/**
* @param {?number} start
* @param {?number} end
*/
_setWindowPosition(start, end) {
const clientWidth = this._parentElement.clientWidth;
const windowLeft = typeof start === 'number' ? start / clientWidth : this.windowLeft;
const windowRight = typeof end === 'number' ? end / clientWidth : this.windowRight;
this._setWindow(windowLeft, windowRight);
}
/**
* @param {!Event} event
*/
_onMouseWheel(event) {
if (!this._enabled) {
return;
}
if (typeof event.wheelDeltaY === 'number' && event.wheelDeltaY) {
const zoomFactor = 1.1;
const mouseWheelZoomSpeed = 1 / 120;
const reference = event.offsetX / event.target.clientWidth;
this._zoom(Math.pow(zoomFactor, -event.wheelDeltaY * mouseWheelZoomSpeed), reference);
}
if (typeof event.wheelDeltaX === 'number' && event.wheelDeltaX) {
let offset = Math.round(event.wheelDeltaX * PerfUI.OverviewGrid.WindowScrollSpeedFactor);
const windowLeft = this._leftResizeElement.offsetLeft + PerfUI.OverviewGrid.ResizerOffset;
const windowRight = this._rightResizeElement.offsetLeft + PerfUI.OverviewGrid.ResizerOffset;
if (windowLeft - offset < 0) {
offset = windowLeft;
}
if (windowRight - offset > this._parentElement.clientWidth) {
offset = windowRight - this._parentElement.clientWidth;
}
this._setWindowPosition(windowLeft - offset, windowRight - offset);
event.preventDefault();
}
}
/**
* @param {number} factor
* @param {number} reference
*/
_zoom(factor, reference) {
let left = this.windowLeft;
let right = this.windowRight;
const windowSize = right - left;
let newWindowSize = factor * windowSize;
if (newWindowSize > 1) {
newWindowSize = 1;
factor = newWindowSize / windowSize;
}
left = reference + (left - reference) * factor;
left = Number.constrain(left, 0, 1 - newWindowSize);
right = reference + (right - reference) * factor;
right = Number.constrain(right, newWindowSize, 1);
this._setWindow(left, right);
}
};
/** @enum {symbol} */
PerfUI.OverviewGrid.Events = {
WindowChanged: Symbol('WindowChanged')
};
/**
* @unrestricted
*/
PerfUI.OverviewGrid.WindowSelector = class {
constructor(parent, position) {
this._startPosition = position;
this._width = parent.offsetWidth;
this._windowSelector = createElement('div');
this._windowSelector.className = 'overview-grid-window-selector';
this._windowSelector.style.left = this._startPosition + 'px';
this._windowSelector.style.right = this._width - this._startPosition + 'px';
parent.appendChild(this._windowSelector);
}
_close(position) {
position = Math.max(0, Math.min(position, this._width));
this._windowSelector.remove();
return this._startPosition < position ? {start: this._startPosition, end: position} :
{start: position, end: this._startPosition};
}
_updatePosition(position) {
position = Math.max(0, Math.min(position, this._width));
if (position < this._startPosition) {
this._windowSelector.style.left = position + 'px';
this._windowSelector.style.right = this._width - this._startPosition + 'px';
} else {
this._windowSelector.style.left = this._startPosition + 'px';
this._windowSelector.style.right = this._width - position + 'px';
}
}
};