chrome-devtools-frontend
Version:
Chrome DevTools UI
273 lines (248 loc) • 10.3 kB
text/typescript
/*
* 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.
*/
/* eslint-disable rulesdir/no_underscored_properties */
import * as Common from '../common/common.js';
import * as i18n from '../i18n/i18n.js';
import * as SDK from '../sdk/sdk.js'; // eslint-disable-line no-unused-vars
import * as UI from '../ui/ui.js';
import {LayerSelection, LayerView, LayerViewHost, Selection, SnapshotSelection} from './LayerViewHost.js'; // eslint-disable-line no-unused-vars
export const UIStrings = {
/**
*@description Label for layers sidepanel tree
*/
layersTreePane: 'Layers Tree Pane',
/**
*@description A context menu item in the DView of the Layers panel
*/
showPaintProfiler: 'Show Paint Profiler',
/**
*@description Details text content in Layer Tree Outline of the Layers panel
*@example {10} PH1
*@example {10} PH2
*/
updateChildDimension: ' ({PH1} × {PH2})',
};
const str_ = i18n.i18n.registerUIStrings('layer_viewer/LayerTreeOutline.ts', UIStrings);
const i18nString = i18n.i18n.getLocalizedString.bind(undefined, str_);
export class LayerTreeOutline extends Common.ObjectWrapper.ObjectWrapper implements LayerView {
_layerViewHost: LayerViewHost;
_treeOutline: UI.TreeOutline.TreeOutlineInShadow;
_lastHoveredNode: LayerTreeElement|null;
element: HTMLElement;
_layerTree?: SDK.LayerTreeBase.LayerTreeBase|null;
_layerSnapshotMap?: Map<SDK.LayerTreeBase.Layer, SnapshotSelection>;
constructor(layerViewHost: LayerViewHost) {
super();
this._layerViewHost = layerViewHost;
this._layerViewHost.registerView(this);
this._treeOutline = new UI.TreeOutline.TreeOutlineInShadow();
this._treeOutline.element.classList.add('layer-tree', 'overflow-auto');
this._treeOutline.element.addEventListener('mousemove', this._onMouseMove.bind(this) as EventListener, false);
this._treeOutline.element.addEventListener('mouseout', this._onMouseMove.bind(this) as EventListener, false);
this._treeOutline.element.addEventListener('contextmenu', this._onContextMenu.bind(this) as EventListener, true);
UI.ARIAUtils.setAccessibleName(this._treeOutline.contentElement, i18nString(UIStrings.layersTreePane));
this._lastHoveredNode = null;
this.element = this._treeOutline.element;
this._layerViewHost.showInternalLayersSetting().addChangeListener(this._update, this);
}
focus(): void {
this._treeOutline.focus();
}
selectObject(selection: Selection|null): void {
this.hoverObject(null);
const layer = selection && selection.layer();
const node = layer && layerToTreeElement.get(layer);
if (node) {
node.revealAndSelect(true);
} else if (this._treeOutline.selectedTreeElement) {
this._treeOutline.selectedTreeElement.deselect();
}
}
hoverObject(selection: Selection|null): void {
const layer = selection && selection.layer();
const node = layer && layerToTreeElement.get(layer);
if (node === this._lastHoveredNode) {
return;
}
if (this._lastHoveredNode) {
this._lastHoveredNode.setHovered(false);
}
if (node) {
node.setHovered(true);
}
this._lastHoveredNode = node as LayerTreeElement;
}
setLayerTree(layerTree: SDK.LayerTreeBase.LayerTreeBase|null): void {
this._layerTree = layerTree;
this._update();
}
_update(): void {
const showInternalLayers = this._layerViewHost.showInternalLayersSetting().get();
const seenLayers = new Map<SDK.LayerTreeBase.Layer, boolean>();
let root: (SDK.LayerTreeBase.Layer|null)|null = null;
if (this._layerTree) {
if (!showInternalLayers) {
root = this._layerTree.contentRoot();
}
if (!root) {
root = this._layerTree.root();
}
}
function updateLayer(this: LayerTreeOutline, layer: SDK.LayerTreeBase.Layer): void {
if (!layer.drawsContent() && !showInternalLayers) {
return;
}
if (seenLayers.get(layer)) {
console.assert(false, 'Duplicate layer: ' + layer.id());
}
seenLayers.set(layer, true);
let node: LayerTreeElement|null = layerToTreeElement.get(layer) || null;
let parentLayer = layer.parent();
// Skip till nearest visible ancestor.
while (parentLayer && parentLayer !== root && !parentLayer.drawsContent() && !showInternalLayers) {
parentLayer = parentLayer.parent();
}
const parent =
layer === root ? this._treeOutline.rootElement() : parentLayer && layerToTreeElement.get(parentLayer);
if (!parent) {
console.assert(false, 'Parent is not in the tree');
return;
}
if (!node) {
node = new LayerTreeElement(this, layer);
parent.appendChild(node);
// Expand all new non-content layers to expose content layers better.
if (!layer.drawsContent()) {
node.expand();
}
} else {
if (node.parent !== parent) {
const oldSelection = this._treeOutline.selectedTreeElement;
if (node.parent) {
node.parent.removeChild(node);
}
parent.appendChild(node);
if (oldSelection && oldSelection !== this._treeOutline.selectedTreeElement) {
oldSelection.select();
}
}
node._update();
}
}
if (root && this._layerTree) {
this._layerTree.forEachLayer(updateLayer.bind(this), root);
}
// Cleanup layers that don't exist anymore from tree.
const rootElement = this._treeOutline.rootElement();
for (let node = rootElement.firstChild(); node instanceof LayerTreeElement && !node.root;) {
if (seenLayers.get(node._layer)) {
node = node.traverseNextTreeElement(false);
} else {
const nextNode = node.nextSibling || node.parent;
if (node.parent) {
node.parent.removeChild(node);
}
if (node === this._lastHoveredNode) {
this._lastHoveredNode = null;
}
node = nextNode;
}
}
if (!this._treeOutline.selectedTreeElement && this._layerTree) {
const elementToSelect = this._layerTree.contentRoot() || this._layerTree.root();
if (elementToSelect) {
const layer = layerToTreeElement.get(elementToSelect);
if (layer) {
layer.revealAndSelect(true);
}
}
}
}
_onMouseMove(event: MouseEvent): void {
const node = this._treeOutline.treeElementFromEvent(event) as LayerTreeElement | null;
if (node === this._lastHoveredNode) {
return;
}
this._layerViewHost.hoverObject(this._selectionForNode(node));
}
_selectedNodeChanged(node: LayerTreeElement): void {
this._layerViewHost.selectObject(this._selectionForNode(node));
}
_onContextMenu(event: MouseEvent): void {
const selection = this._selectionForNode(this._treeOutline.treeElementFromEvent(event) as LayerTreeElement | null);
const contextMenu = new UI.ContextMenu.ContextMenu(event);
const layer = selection && selection.layer();
if (layer) {
this._layerSnapshotMap = this._layerViewHost.getLayerSnapshotMap();
if (this._layerSnapshotMap.has(layer)) {
contextMenu.defaultSection().appendItem(
i18nString(UIStrings.showPaintProfiler),
this.dispatchEventToListeners.bind(this, Events.PaintProfilerRequested, selection), false);
}
}
this._layerViewHost.showContextMenu(contextMenu, selection);
}
_selectionForNode(node: LayerTreeElement|null): Selection|null {
return node && node._layer ? new LayerSelection(node._layer) : null;
}
}
// TODO(crbug.com/1167717): Make this a const enum again
// eslint-disable-next-line rulesdir/const_enum
export enum Events {
PaintProfilerRequested = 'PaintProfilerRequested',
}
export class LayerTreeElement extends UI.TreeOutline.TreeElement {
_treeOutline: LayerTreeOutline;
_layer: SDK.LayerTreeBase.Layer;
constructor(tree: LayerTreeOutline, layer: SDK.LayerTreeBase.Layer) {
super();
this._treeOutline = tree;
this._layer = layer;
layerToTreeElement.set(layer, this);
this._update();
}
_update(): void {
const node = this._layer.nodeForSelfOrAncestor();
const title = document.createDocumentFragment();
UI.UIUtils.createTextChild(title, node ? node.simpleSelector() : '#' + this._layer.id());
const details = title.createChild('span', 'dimmed');
details.textContent =
i18nString(UIStrings.updateChildDimension, {PH1: this._layer.width(), PH2: this._layer.height()});
this.title = title;
}
onselect(): boolean {
this._treeOutline._selectedNodeChanged(this);
return false;
}
setHovered(hovered: boolean): void {
this.listItemElement.classList.toggle('hovered', hovered);
}
}
export const layerToTreeElement = new WeakMap<SDK.LayerTreeBase.Layer, LayerTreeElement>();