1/*
2 * Copyright (C) 2013 Apple 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
6 * are met:
7 * 1. Redistributions of source code must retain the above copyright
8 *    notice, this list of conditions and the following disclaimer.
9 * 2. Redistributions in binary form must reproduce the above copyright
10 *    notice, this list of conditions and the following disclaimer in the
11 *    documentation and/or other materials provided with the distribution.
12 *
13 * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
14 * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
15 * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
16 * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
17 * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
18 * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
19 * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
20 * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
21 * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
22 * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
23 * THE POSSIBILITY OF SUCH DAMAGE.
24 */
25
26WebInspector.LayerTreeSidebarPanel = function() {
27    WebInspector.DOMDetailsSidebarPanel.call(this, "layer-tree", WebInspector.UIString("Layers"), WebInspector.UIString("Layer"), "Images/NavigationItemLayers.svg", "3");
28
29    this._dataGridNodesByLayerId = {};
30
31    this.element.classList.add(WebInspector.LayerTreeSidebarPanel.StyleClassName);
32
33    WebInspector.showShadowDOMSetting.addEventListener(WebInspector.Setting.Event.Changed, this._showShadowDOMSettingChanged, this);
34
35    window.addEventListener("resize", this._windowResized.bind(this));
36
37    this._buildLayerInfoSection();
38    this._buildDataGridSection();
39    this._buildBottomBar();
40};
41
42WebInspector.LayerTreeSidebarPanel.StyleClassName = "layer-tree";
43
44WebInspector.LayerTreeSidebarPanel.prototype = {
45    constructor: WebInspector.LayerTreeSidebarPanel,
46
47    // DetailsSidebarPanel Overrides.
48
49    shown: function()
50    {
51        WebInspector.layerTreeManager.addEventListener(WebInspector.LayerTreeManager.Event.LayerTreeDidChange, this._layerTreeDidChange, this);
52
53        console.assert(this.parentSidebar);
54
55        this.needsRefresh();
56
57        WebInspector.DOMDetailsSidebarPanel.prototype.shown.call(this);
58    },
59
60    hidden: function()
61    {
62        WebInspector.layerTreeManager.removeEventListener(WebInspector.LayerTreeManager.Event.LayerTreeDidChange, this._layerTreeDidChange, this);
63
64        WebInspector.DOMDetailsSidebarPanel.prototype.hidden.call(this);
65    },
66
67    refresh: function()
68    {
69        if (!this.domNode)
70            return;
71
72        WebInspector.layerTreeManager.layersForNode(this.domNode, function(layerForNode, childLayers) {
73            this._unfilteredChildLayers = childLayers;
74            this._updateDisplayWithLayers(layerForNode, childLayers);
75        }.bind(this));
76    },
77
78    // DOMDetailsSidebarPanel Overrides
79
80    supportsDOMNode: function(nodeToInspect)
81    {
82        return WebInspector.layerTreeManager.supported && nodeToInspect.nodeType() === Node.ELEMENT_NODE;
83    },
84
85    // Private
86
87    _layerTreeDidChange: function(event)
88    {
89        this.needsRefresh();
90    },
91
92    _showShadowDOMSettingChanged: function(event)
93    {
94        if (this.selected)
95            this._updateDisplayWithLayers(this._layerForNode, this._unfilteredChildLayers);
96    },
97
98    _windowResized: function(event)
99    {
100        if (this._popover && this._popover.visible)
101            this._updatePopoverForSelectedNode();
102    },
103
104    _buildLayerInfoSection: function()
105    {
106        var rows = this._layerInfoRows = {};
107        var rowsArray = [];
108
109        rowsArray.push(rows["Width"] = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Width")));
110        rowsArray.push(rows["Height"] = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Height")));
111        rowsArray.push(rows["Paints"] = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Paints")));
112        rowsArray.push(rows["Memory"] = new WebInspector.DetailsSectionSimpleRow(WebInspector.UIString("Memory")));
113
114        this._layerInfoGroup = new WebInspector.DetailsSectionGroup(rowsArray);
115
116        var emptyRow = new WebInspector.DetailsSectionRow(WebInspector.UIString("No Layer Available"));
117        emptyRow.showEmptyMessage();
118        this._noLayerInformationGroup = new WebInspector.DetailsSectionGroup([emptyRow]);
119
120        this._layerInfoSection = new WebInspector.DetailsSection("layer-info", WebInspector.UIString("Layer Info"), [this._noLayerInformationGroup]);
121
122        this.element.appendChild(this._layerInfoSection.element);
123    },
124
125    _buildDataGridSection: function()
126    {
127        var columns = {name: {}, paintCount: {}, memory: {}};
128
129        columns.name.title = WebInspector.UIString("Node");
130        columns.name.sortable = false;
131
132        columns.paintCount.title = WebInspector.UIString("Paints");
133        columns.paintCount.sortable = true;
134        columns.paintCount.aligned = "right";
135        columns.paintCount.width = "50px";
136
137        columns.memory.title = WebInspector.UIString("Memory");
138        columns.memory.sortable = true;
139        columns.memory.aligned = "right";
140        columns.memory.width = "70px";
141
142        this._dataGrid = new WebInspector.DataGrid(columns);
143        this._dataGrid.addEventListener(WebInspector.DataGrid.Event.SortChanged, this._sortDataGrid, this);
144        this._dataGrid.addEventListener(WebInspector.DataGrid.Event.SelectedNodeChanged, this._selectedDataGridNodeChanged, this);
145
146        this.sortColumnIdentifier = "memory";
147        this.sortOrder = WebInspector.DataGrid.SortOrder.Descending;
148
149        var element = this._dataGrid.element;
150        element.classList.add("inline");
151        element.addEventListener("focus", this._dataGridGainedFocus.bind(this), false);
152        element.addEventListener("blur", this._dataGridLostFocus.bind(this), false);
153        element.addEventListener("click", this._dataGridWasClicked.bind(this), false);
154
155        this._childLayersRow = new WebInspector.DetailsSectionDataGridRow(null, WebInspector.UIString("No Child Layers"));
156        var group = new WebInspector.DetailsSectionGroup([this._childLayersRow]);
157        var section = new WebInspector.DetailsSection("layer-children", WebInspector.UIString("Child Layers"), [group], null, true);
158
159        var element = this.element.appendChild(section.element);
160        element.classList.add(section.identifier);
161    },
162
163    _buildBottomBar: function()
164    {
165        var bottomBar = this.element.appendChild(document.createElement("div"));
166        bottomBar.className = "bottom-bar";
167
168        this._layersCountLabel = bottomBar.appendChild(document.createElement("div"));
169        this._layersCountLabel.className = "layers-count-label";
170
171        this._layersMemoryLabel = bottomBar.appendChild(document.createElement("div"));
172        this._layersMemoryLabel.className = "layers-memory-label";
173    },
174
175    _sortDataGrid: function()
176    {
177        var sortColumnIdentifier = this._dataGrid.sortColumnIdentifier;
178
179        function comparator(a, b)
180        {
181            var item1 = a.layer[sortColumnIdentifier] || 0;
182            var item2 = b.layer[sortColumnIdentifier] || 0;
183            return item1 - item2;
184        };
185
186        this._dataGrid.sortNodes(comparator);
187        this._updatePopoverForSelectedNode();
188    },
189
190    _selectedDataGridNodeChanged: function()
191    {
192        if (this._dataGrid.selectedNode) {
193            this._highlightSelectedNode();
194            this._showPopoverForSelectedNode();
195        } else {
196            WebInspector.domTreeManager.hideDOMNodeHighlight();
197            this._hidePopover();
198        }
199    },
200
201    _dataGridGainedFocus: function(event)
202    {
203        this._highlightSelectedNode();
204        this._showPopoverForSelectedNode();
205    },
206
207    _dataGridLostFocus: function(event)
208    {
209        WebInspector.domTreeManager.hideDOMNodeHighlight();
210        this._hidePopover();
211    },
212
213    _dataGridWasClicked: function(event)
214    {
215        if (this._dataGrid.selectedNode && event.target.parentNode.classList.contains("filler"))
216            this._dataGrid.selectedNode.deselect();
217    },
218
219    _highlightSelectedNode: function()
220    {
221        var dataGridNode = this._dataGrid.selectedNode;
222        if (!dataGridNode)
223            return;
224
225        var layer = dataGridNode.layer;
226        if (layer.isGeneratedContent || layer.isReflection || layer.isAnonymous)
227            WebInspector.domTreeManager.highlightRect(layer.bounds, true);
228        else
229            WebInspector.domTreeManager.highlightDOMNode(layer.nodeId);
230    },
231
232    _updateDisplayWithLayers: function(layerForNode, childLayers)
233    {
234        if (!WebInspector.showShadowDOMSetting.value) {
235            childLayers = childLayers.filter(function(layer) {
236                return !layer.isInShadowTree;
237            });
238        }
239
240        this._updateLayerInfoSection(layerForNode);
241        this._updateDataGrid(layerForNode, childLayers);
242        this._updateMetrics(layerForNode, childLayers);
243
244        this._layerForNode = layerForNode;
245        this._childLayers = childLayers;
246    },
247
248    _updateLayerInfoSection: function(layer)
249    {
250        const emDash = "\u2014";
251
252        this._layerInfoSection.groups = layer ? [this._layerInfoGroup] : [this._noLayerInformationGroup];
253
254        if (!layer)
255            return;
256
257        this._layerInfoRows["Memory"].value = Number.bytesToString(layer.memory);
258        this._layerInfoRows["Width"].value = layer.compositedBounds.width + "px";
259        this._layerInfoRows["Height"].value = layer.compositedBounds.height + "px";
260        this._layerInfoRows["Paints"].value = layer.paintCount + "";
261    },
262
263    _updateDataGrid: function(layerForNode, childLayers)
264    {
265        var dataGrid = this._dataGrid;
266
267        var mutations = WebInspector.layerTreeManager.layerTreeMutations(this._childLayers, childLayers);
268
269        mutations.removals.forEach(function(layer) {
270            var node = this._dataGridNodesByLayerId[layer.layerId];
271            if (node) {
272                dataGrid.removeChild(node);
273                delete this._dataGridNodesByLayerId[layer.layerId];
274            }
275        }.bind(this));
276
277        mutations.additions.forEach(function(layer) {
278            var node = this._dataGridNodeForLayer(layer);
279            if (node)
280                dataGrid.appendChild(node);
281        }.bind(this));
282
283        mutations.preserved.forEach(function(layer) {
284            var node = this._dataGridNodesByLayerId[layer.layerId];
285            if (node)
286                node.layer = layer;
287        }.bind(this));
288
289        this._sortDataGrid();
290
291        this._childLayersRow.dataGrid = !isEmptyObject(childLayers) ? this._dataGrid : null;
292    },
293
294    _dataGridNodeForLayer: function(layer)
295    {
296        var node = new WebInspector.LayerTreeDataGridNode(layer);
297
298        this._dataGridNodesByLayerId[layer.layerId] = node;
299
300        return node;
301    },
302
303    _updateMetrics: function(layerForNode, childLayers)
304    {
305        var layerCount = 0;
306        var totalMemory = 0;
307
308        if (layerForNode) {
309            layerCount++;
310            totalMemory += layerForNode.memory || 0;
311        }
312
313        childLayers.forEach(function(layer) {
314            layerCount++;
315            totalMemory += layer.memory || 0;
316        });
317
318        this._layersCountLabel.textContent = WebInspector.UIString("Layer Count: %d").format(layerCount);
319        this._layersMemoryLabel.textContent = WebInspector.UIString("Memory: %s").format(Number.bytesToString(totalMemory));
320    },
321
322    _showPopoverForSelectedNode: function()
323    {
324        var dataGridNode = this._dataGrid.selectedNode;
325        if (!dataGridNode)
326            return;
327
328        this._contentForPopover(dataGridNode.layer, function(content) {
329            if (dataGridNode === this._dataGrid.selectedNode)
330                this._updatePopoverForSelectedNode(content);
331        }.bind(this));
332    },
333
334    _updatePopoverForSelectedNode: function(content)
335    {
336        var dataGridNode = this._dataGrid.selectedNode;
337        if (!dataGridNode)
338            return;
339
340        var popover = this._popover;
341        if (!popover)
342            popover = this._popover = new WebInspector.Popover;
343
344        var targetFrame = WebInspector.Rect.rectFromClientRect(dataGridNode.element.getBoundingClientRect());
345
346        if (content)
347            popover.content = content;
348
349        popover.present(targetFrame.pad(2), [WebInspector.RectEdge.MIN_X]);
350    },
351
352    _hidePopover: function()
353    {
354        if (this._popover)
355            this._popover.dismiss();
356    },
357
358    _contentForPopover: function(layer, callback)
359    {
360        var content = document.createElement("div");
361        content.className = "layer-tree-popover";
362
363        content.appendChild(document.createElement("p")).textContent = WebInspector.UIString("Reasons for compositing:");
364
365        var list = content.appendChild(document.createElement("ul"));
366
367        WebInspector.layerTreeManager.reasonsForCompositingLayer(layer, function(compositingReasons) {
368            if (isEmptyObject(compositingReasons)) {
369                callback(content);
370                return;
371            }
372
373            this._populateListOfCompositingReasons(list, compositingReasons);
374
375            callback(content);
376        }.bind(this));
377
378        return content;
379    },
380
381    _populateListOfCompositingReasons: function(list, compositingReasons)
382    {
383        function addReason(reason)
384        {
385            list.appendChild(document.createElement("li")).textContent = reason;
386        }
387
388        if (compositingReasons.transform3D)
389            addReason(WebInspector.UIString("Element has a 3D transform"));
390        if (compositingReasons.video)
391            addReason(WebInspector.UIString("Element is <video>"));
392        if (compositingReasons.canvas)
393            addReason(WebInspector.UIString("Element is <canvas>"));
394        if (compositingReasons.plugin)
395            addReason(WebInspector.UIString("Element is a plug-in"));
396        if (compositingReasons.iFrame)
397            addReason(WebInspector.UIString("Element is <iframe>"));
398        if (compositingReasons.backfaceVisibilityHidden)
399            addReason(WebInspector.UIString("Element has “backface-visibility: hidden” style"));
400        if (compositingReasons.clipsCompositingDescendants)
401            addReason(WebInspector.UIString("Element clips compositing descendants"));
402        if (compositingReasons.animation)
403            addReason(WebInspector.UIString("Element is animated"));
404        if (compositingReasons.filters)
405            addReason(WebInspector.UIString("Element has CSS filters applied"));
406        if (compositingReasons.positionFixed)
407            addReason(WebInspector.UIString("Element has “position: fixed” style"));
408        if (compositingReasons.positionSticky)
409            addReason(WebInspector.UIString("Element has “position: sticky” style"));
410        if (compositingReasons.overflowScrollingTouch)
411            addReason(WebInspector.UIString("Element has “-webkit-overflow-scrolling: touch” style"));
412        if (compositingReasons.stacking)
413            addReason(WebInspector.UIString("Element establishes a stacking context"));
414        if (compositingReasons.overlap)
415            addReason(WebInspector.UIString("Element overlaps other compositing element"));
416        if (compositingReasons.negativeZIndexChildren)
417            addReason(WebInspector.UIString("Element has children with a negative z-index"));
418        if (compositingReasons.transformWithCompositedDescendants)
419            addReason(WebInspector.UIString("Element has a 2D transform and composited descendants"));
420        if (compositingReasons.opacityWithCompositedDescendants)
421            addReason(WebInspector.UIString("Element has opacity applied and composited descendants"));
422        if (compositingReasons.maskWithCompositedDescendants)
423            addReason(WebInspector.UIString("Element is masked and composited descendants"));
424        if (compositingReasons.reflectionWithCompositedDescendants)
425            addReason(WebInspector.UIString("Element has a reflection and composited descendants"));
426        if (compositingReasons.filterWithCompositedDescendants)
427            addReason(WebInspector.UIString("Element has CSS filters applied and composited descendants"));
428        if (compositingReasons.blendingWithCompositedDescendants)
429            addReason(WebInspector.UIString("Element has CSS blending applied and composited descendants"));
430        if (compositingReasons.isolatesCompositedBlendingDescendants)
431            addReason(WebInspector.UIString("Element is a stacking context and has composited descendants with CSS blending applied"));
432        if (compositingReasons.perspective)
433            addReason(WebInspector.UIString("Element has perspective applied"));
434        if (compositingReasons.preserve3D)
435            addReason(WebInspector.UIString("Element has “transform-style: preserve-3d” style"));
436        if (compositingReasons.root)
437            addReason(WebInspector.UIString("Element is the root element"));
438        if (compositingReasons.blending)
439            addReason(WebInspector.UIString("Element has “blend-mode” style"));
440    }
441};
442
443WebInspector.LayerTreeSidebarPanel.prototype.__proto__ = WebInspector.DOMDetailsSidebarPanel.prototype;
444