DevTools [Layers]: Adding paint profiler link for sidepanel nodes

Re-upload of https://chromium-review.googlesource.com/c/chromium/src/+/1626027 to new repository

Issue:
- Paint profiler link is only available when selecting a node on the 3D canvas
- 3D canvas is only cursor accessible
Changes:
- Made Paint Profile tab keyboard accessible (via link and sidebar context menu)

Changing PaintProfilerLink from padding to margin (focus indicator):
https://imgur.com/a/B6kATQS

Bug: 963183
Change-Id: I79498b210539f9c2c8a114ef323f66f8baf61b92
Reviewed-on: https://chromium-review.googlesource.com/c/devtools/devtools-frontend/+/1872877
Reviewed-by: Benedikt Meurer <[email protected]>
Commit-Queue: Michael Liao <[email protected]>
diff --git a/front_end/layer_viewer/LayerDetailsView.js b/front_end/layer_viewer/LayerDetailsView.js
index 268a15e..8c8fb09 100644
--- a/front_end/layer_viewer/LayerDetailsView.js
+++ b/front_end/layer_viewer/LayerDetailsView.js
@@ -41,6 +41,7 @@
     this._layerViewHost = layerViewHost;
     this._layerViewHost.registerView(this);
     this._emptyWidget = new UI.EmptyWidget(Common.UIString('Select a layer to see its details'));
+    this._layerSnapshotMap = this._layerViewHost.getLayerSnapshotMap();
     this._buildContent();
   }
 
@@ -88,9 +89,12 @@
     this._layerViewHost.selectObject(new LayerViewer.LayerView.ScrollRectSelection(this._selection.layer(), index));
   }
 
-  _onPaintProfilerButtonClicked() {
-    if (this._selection.type() === LayerViewer.LayerView.Selection.Type.Snapshot || this._selection.layer()) {
-      this.dispatchEventToListeners(LayerViewer.LayerDetailsView.Events.PaintProfilerRequested, this._selection);
+  _invokeProfilerLink() {
+    const snapshotSelection = this._selection.type() === LayerViewer.LayerView.Selection.Type.Snapshot ?
+        this._selection :
+        this._layerSnapshotMap.get(this._selection.layer());
+    if (snapshotSelection) {
+      this.dispatchEventToListeners(LayerViewer.LayerDetailsView.Events.PaintProfilerRequested, snapshotSelection);
     }
   }
 
@@ -173,13 +177,13 @@
     const layer = this._selection && this._selection.layer();
     if (!layer) {
       this._tableElement.remove();
-      this._paintProfilerButton.remove();
+      this._paintProfilerLink.remove();
       this._emptyWidget.show(this.contentElement);
       return;
     }
     this._emptyWidget.detach();
     this.contentElement.appendChild(this._tableElement);
-    this.contentElement.appendChild(this._paintProfilerButton);
+    this.contentElement.appendChild(this._paintProfilerLink);
     this._sizeCell.textContent =
         Common.UIString('%d × %d (at %d,%d)', layer.width(), layer.height(), layer.offsetX(), layer.offsetY());
     this._paintCountCell.parentElement.classList.toggle('hidden', !layer.paintCount());
@@ -192,7 +196,8 @@
     const snapshot = this._selection.type() === LayerViewer.LayerView.Selection.Type.Snapshot ?
         /** @type {!LayerViewer.LayerView.SnapshotSelection} */ (this._selection).snapshot() :
         null;
-    this._paintProfilerButton.classList.toggle('hidden', !snapshot);
+
+    this._paintProfilerLink.classList.toggle('hidden', !(this._layerSnapshotMap.has(layer) || snapshot));
   }
 
   _buildContent() {
@@ -204,9 +209,20 @@
     this._paintCountCell = this._createRow(Common.UIString('Paint count'));
     this._scrollRectsCell = this._createRow(Common.UIString('Slow scroll regions'));
     this._stickyPositionConstraintCell = this._createRow(Common.UIString('Sticky position constraint'));
-    this._paintProfilerButton = this.contentElement.createChild('a', 'hidden link');
-    this._paintProfilerButton.textContent = Common.UIString('Paint Profiler');
-    this._paintProfilerButton.addEventListener('click', this._onPaintProfilerButtonClicked.bind(this));
+    this._paintProfilerLink = this.contentElement.createChild('span', 'hidden devtools-link link-margin');
+    UI.ARIAUtils.markAsLink(this._paintProfilerLink);
+    this._paintProfilerLink.textContent = ls`Paint Profiler`;
+    this._paintProfilerLink.tabIndex = 0;
+    this._paintProfilerLink.addEventListener('click', e => {
+      e.consume(true);
+      this._invokeProfilerLink();
+    });
+    this._paintProfilerLink.addEventListener('keydown', event => {
+      if (isEnterKey(event)) {
+        event.consume();
+        this._invokeProfilerLink();
+      }
+    });
   }
 
   /**
diff --git a/front_end/layer_viewer/LayerTreeOutline.js b/front_end/layer_viewer/LayerTreeOutline.js
index 5d88ddf..8abedba 100644
--- a/front_end/layer_viewer/LayerTreeOutline.js
+++ b/front_end/layer_viewer/LayerTreeOutline.js
@@ -45,6 +45,7 @@
     this._treeOutline.element.addEventListener('mousemove', this._onMouseMove.bind(this), false);
     this._treeOutline.element.addEventListener('mouseout', this._onMouseMove.bind(this), false);
     this._treeOutline.element.addEventListener('contextmenu', this._onContextMenu.bind(this), true);
+    UI.ARIAUtils.setAccessibleName(this._treeOutline.contentElement, ls`Layers Tree Pane`);
 
     this._lastHoveredNode = null;
     this.element = this._treeOutline.element;
@@ -205,6 +206,17 @@
   _onContextMenu(event) {
     const selection = this._selectionForNode(this._treeOutline.treeElementFromEvent(event));
     const contextMenu = new UI.ContextMenu(event);
+    const layer = selection && selection.layer();
+    if (layer) {
+      this._layerSnapshotMap = this._layerViewHost.getLayerSnapshotMap();
+      if (this._layerSnapshotMap.has(layer)) {
+        contextMenu.defaultSection().appendItem(
+            ls`Show Paint Profiler`,
+            this.dispatchEventToListeners.bind(
+                this, LayerViewer.LayerTreeOutline.Events.PaintProfilerRequested, selection),
+            false);
+      }
+    }
     this._layerViewHost.showContextMenu(contextMenu, selection);
   }
 
@@ -218,6 +230,13 @@
 };
 
 /**
+ * @enum {symbol}
+ */
+LayerViewer.LayerTreeOutline.Events = {
+  PaintProfilerRequested: Symbol('PaintProfilerRequested')
+};
+
+/**
  * @unrestricted
  */
 LayerViewer.LayerTreeElement = class extends UI.TreeElement {
diff --git a/front_end/layer_viewer/LayerViewHost.js b/front_end/layer_viewer/LayerViewHost.js
index f92b464..40e0bee 100644
--- a/front_end/layer_viewer/LayerViewHost.js
+++ b/front_end/layer_viewer/LayerViewHost.js
@@ -177,6 +177,20 @@
   }
 
   /**
+   * @param {!Map<!SDK.Layer, !LayerViewer.LayerView.SnapshotSelection>} snapshotLayers
+   */
+  setLayerSnapshotMap(snapshotLayers) {
+    this._snapshotLayers = snapshotLayers;
+  }
+
+  /**
+   * @return {!Map<!SDK.Layer, !LayerViewer.LayerView.SnapshotSelection>}
+   */
+  getLayerSnapshotMap() {
+    return this._snapshotLayers;
+  }
+
+  /**
    * @param {?SDK.LayerTreeBase} layerTree
    */
   setLayerTree(layerTree) {
diff --git a/front_end/layer_viewer/Layers3DView.js b/front_end/layer_viewer/Layers3DView.js
index b68f022..ff80a5e 100644
--- a/front_end/layer_viewer/Layers3DView.js
+++ b/front_end/layer_viewer/Layers3DView.js
@@ -69,6 +69,10 @@
     this._chromeTextures = [];
     this._rects = [];
 
+    /** @type Map<SDK.Layer, LayerViewer.LayerView.SnapshotSelection> */
+    this._snapshotLayers = new Map();
+    this._layerViewHost.setLayerSnapshotMap(this._snapshotLayers);
+
     this._layerViewHost.showInternalLayersSetting().addChangeListener(this._update, this);
   }
 
@@ -462,6 +466,10 @@
       }
       const selection = new LayerViewer.LayerView.SnapshotSelection(layer, {rect: tile.rect, snapshot: tile.snapshot});
       const rect = new LayerViewer.Layers3DView.Rectangle(selection);
+      if (!this._snapshotLayers.has(layer)) {
+        this._snapshotLayers.set(layer, selection);
+      }
+
       rect.calculateVerticesFromRect(layer, tile.rect, this._depthForLayer(layer) + 1);
       rect.texture = tile.texture;
       this._appendRect(rect);
@@ -470,6 +478,7 @@
 
   _calculateRects() {
     this._rects = [];
+    this._snapshotLayers.clear();
     this._dimensionsForAutoscale = {width: 0, height: 0};
     this._layerTree.forEachLayer(this._calculateLayerRect.bind(this));
 
diff --git a/front_end/layer_viewer/layerDetailsView.css b/front_end/layer_viewer/layerDetailsView.css
index 2e620dd..57c2d03 100644
--- a/front_end/layer_viewer/layerDetailsView.css
+++ b/front_end/layer_viewer/layerDetailsView.css
@@ -23,7 +23,7 @@
     margin-block-end: 0;
 }
 
-a {
-    padding: 8px;
-    display: block;
+.devtools-link.link-margin {
+    margin: 8px;
+    display: inline-block;
 }
diff --git a/front_end/layer_viewer/layer_viewer_strings.grdp b/front_end/layer_viewer/layer_viewer_strings.grdp
index b110d59..d79a639 100644
--- a/front_end/layer_viewer/layer_viewer_strings.grdp
+++ b/front_end/layer_viewer/layer_viewer_strings.grdp
@@ -93,6 +93,9 @@
   <message name="IDS_DEVTOOLS_c634209b1884c963528d21a21cb64ac8" desc="Text in Layer Details View of the Layers panel">
     Nearest Layer Shifting Containing Block
   </message>
+  <message name="IDS_DEVTOOLS_c874f8e5eb4a8b49e80cc524acde3f23" desc="Label for layers sidepanel tree">
+    Layers Tree Pane
+  </message>
   <message name="IDS_DEVTOOLS_cd5be434fea99c341680590738e3b6d5" desc="Text in DView of the Layers panel">
     WebGL support is disabled in your browser.
   </message>
diff --git a/front_end/layers/LayersPanel.js b/front_end/layers/LayersPanel.js
index c40ea5c..337c441 100644
--- a/front_end/layers/LayersPanel.js
+++ b/front_end/layers/LayersPanel.js
@@ -41,6 +41,8 @@
     SDK.targetManager.observeTargets(this);
     this._layerViewHost = new LayerViewer.LayerViewHost();
     this._layerTreeOutline = new LayerViewer.LayerTreeOutline(this._layerViewHost);
+    this._layerTreeOutline.addEventListener(
+        LayerViewer.LayerTreeOutline.Events.PaintProfilerRequested, this._onPaintProfileRequested, this);
     this.panelSidebarElement().appendChild(this._layerTreeOutline.element);
     this.setDefaultFocusedElement(this._layerTreeOutline.element);