Bug 1406285 - Part 1: Implement animation target node. r?gl draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Thu, 18 Jan 2018 10:40:50 +0900
changeset 721930 a01701ebf148a1d9f87ab7b5a6628f91d98ea651
parent 721830 b7a651281314d6369658eeb58e3bb181cf95016f
child 721931 7b9453a9313039320e8985a6dda2f5b9c244ef13
push id96003
push userbmo:dakatsuka@mozilla.com
push dateThu, 18 Jan 2018 05:23:36 +0000
reviewersgl
bugs1406285
milestone59.0a1
Bug 1406285 - Part 1: Implement animation target node. r?gl MozReview-Commit-ID: ERQPDce3vjo
devtools/client/inspector/animation/animation.js
devtools/client/inspector/animation/components/AnimationItem.js
devtools/client/inspector/animation/components/AnimationList.js
devtools/client/inspector/animation/components/AnimationListContainer.js
devtools/client/inspector/animation/components/AnimationTarget.js
devtools/client/inspector/animation/components/App.js
devtools/client/inspector/animation/components/moz.build
devtools/client/inspector/animation/test/head.js
devtools/client/inspector/animation/utils/utils.js
devtools/client/themes/animation.css
--- a/devtools/client/inspector/animation/animation.js
+++ b/devtools/client/inspector/animation/animation.js
@@ -3,50 +3,76 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { AnimationsFront } = require("devtools/shared/fronts/animation");
 const { createElement, createFactory } = require("devtools/client/shared/vendor/react");
 const { Provider } = require("devtools/client/shared/vendor/react-redux");
 
+const EventEmitter = require("devtools/shared/event-emitter");
+
 const App = createFactory(require("./components/App"));
-const { isAllTimingEffectEqual } = require("./utils/utils");
 
 const { updateAnimations } = require("./actions/animations");
 const { updateElementPickerEnabled } = require("./actions/element-picker");
 const { updateSidebarSize } = require("./actions/sidebar");
+const { isAllAnimationEqual } = require("./utils/utils");
 
 class AnimationInspector {
   constructor(inspector) {
     this.inspector = inspector;
 
+    this.getNodeFromActor = this.getNodeFromActor.bind(this);
     this.toggleElementPicker = this.toggleElementPicker.bind(this);
     this.update = this.update.bind(this);
     this.onElementPickerStarted = this.onElementPickerStarted.bind(this);
     this.onElementPickerStopped = this.onElementPickerStopped.bind(this);
     this.onSidebarResized = this.onSidebarResized.bind(this);
     this.onSidebarSelect = this.onSidebarSelect.bind(this);
 
+    EventEmitter.decorate(this);
+    this.emit = this.emit.bind(this);
+
     this.init();
   }
 
   init() {
+    const {
+      setSelectedNode,
+      onShowBoxModelHighlighterForNode,
+    } = this.inspector.getCommonComponentProps();
+
+    const {
+      onHideBoxModelHighlighter,
+    } = this.inspector.getPanel("boxmodel").getComponentProps();
+
+    const {
+      emit: emitEventForTest,
+      getNodeFromActor,
+      toggleElementPicker,
+    } = this;
+
     const target = this.inspector.target;
     this.animationsFront = new AnimationsFront(target.client, target.form);
 
     const provider = createElement(Provider,
       {
         id: "newanimationinspector",
         key: "newanimationinspector",
         store: this.inspector.store
       },
       App(
         {
-          toggleElementPicker: this.toggleElementPicker
+          emitEventForTest,
+          getNodeFromActor,
+          onHideBoxModelHighlighter,
+          onShowBoxModelHighlighterForNode,
+          setSelectedNode,
+          toggleElementPicker,
         }
       )
     );
     this.provider = provider;
 
     this.inspector.selection.on("new-node-front", this.update);
     this.inspector.sidebar.on("newanimationinspector-selected", this.onSidebarSelect);
     this.inspector.toolbox.on("inspector-sidebar-resized", this.onSidebarResized);
@@ -73,30 +99,34 @@ class AnimationInspector {
     const done = this.inspector.updating("newanimationinspector");
 
     const selection = this.inspector.selection;
     const animations =
       selection.isConnected() && selection.isElementNode()
       ? await this.animationsFront.getAnimationPlayersForNode(selection.nodeFront)
       : [];
 
-    if (!this.animations || !isAllTimingEffectEqual(animations, this.animations)) {
+    if (!this.animations || !isAllAnimationEqual(animations, this.animations)) {
       this.inspector.store.dispatch(updateAnimations(animations));
       this.animations = animations;
     }
 
     done();
   }
 
   isPanelVisible() {
     return this.inspector && this.inspector.toolbox && this.inspector.sidebar &&
            this.inspector.toolbox.currentToolId === "inspector" &&
            this.inspector.sidebar.getCurrentTabID() === "newanimationinspector";
   }
 
+  getNodeFromActor(actorID) {
+    return this.inspector.walker.getNodeFromActor(actorID, ["node"]);
+  }
+
   toggleElementPicker() {
     this.inspector.toolbox.highlighterUtils.togglePicker();
   }
 
   onElementPickerStarted() {
     this.inspector.store.dispatch(updateElementPickerEnabled(true));
   }
 
--- a/devtools/client/inspector/animation/components/AnimationItem.js
+++ b/devtools/client/inspector/animation/components/AnimationItem.js
@@ -1,27 +1,53 @@
 /* This Source Code Form is subject to the terms of the Mozilla Public
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
-const { PureComponent } = require("devtools/client/shared/vendor/react");
+const { createFactory, PureComponent } = require("devtools/client/shared/vendor/react");
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
+const AnimationTarget = createFactory(require("./AnimationTarget"));
+
 class AnimationItem extends PureComponent {
   static get propTypes() {
     return {
       animation: PropTypes.object.isRequired,
+      emitEventForTest: PropTypes.func.isRequired,
+      getNodeFromActor: PropTypes.func.isRequired,
+      onHideBoxModelHighlighter: PropTypes.func.isRequired,
+      onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
+      setSelectedNode: PropTypes.func.isRequired,
     };
   }
 
   render() {
+    const {
+      animation,
+      emitEventForTest,
+      getNodeFromActor,
+      onHideBoxModelHighlighter,
+      onShowBoxModelHighlighterForNode,
+      setSelectedNode,
+    } = this.props;
+
     return dom.li(
       {
         className: "animation-item"
-      }
+      },
+      AnimationTarget(
+        {
+          animation,
+          emitEventForTest,
+          getNodeFromActor,
+          onHideBoxModelHighlighter,
+          onShowBoxModelHighlighterForNode,
+          setSelectedNode,
+        }
+      )
     );
   }
 }
 
 module.exports = AnimationItem;
--- a/devtools/client/inspector/animation/components/AnimationList.js
+++ b/devtools/client/inspector/animation/components/AnimationList.js
@@ -9,22 +9,47 @@ const dom = require("devtools/client/sha
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 
 const AnimationItem = createFactory(require("./AnimationItem"));
 
 class AnimationList extends PureComponent {
   static get propTypes() {
     return {
       animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+      emitEventForTest: PropTypes.func.isRequired,
+      getNodeFromActor: PropTypes.func.isRequired,
+      onHideBoxModelHighlighter: PropTypes.func.isRequired,
+      onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
+      setSelectedNode: PropTypes.func.isRequired,
     };
   }
 
   render() {
+    const {
+      animations,
+      emitEventForTest,
+      getNodeFromActor,
+      onHideBoxModelHighlighter,
+      onShowBoxModelHighlighterForNode,
+      setSelectedNode,
+    } = this.props;
+
     return dom.ul(
       {
         className: "animation-list"
       },
-      this.props.animations.map(animation => AnimationItem({ animation }))
+      animations.map(animation =>
+        AnimationItem(
+          {
+            animation,
+            emitEventForTest,
+            getNodeFromActor,
+            onHideBoxModelHighlighter,
+            onShowBoxModelHighlighterForNode,
+            setSelectedNode,
+          }
+        )
+      )
     );
   }
 }
 
 module.exports = AnimationList;
--- a/devtools/client/inspector/animation/components/AnimationListContainer.js
+++ b/devtools/client/inspector/animation/components/AnimationListContainer.js
@@ -11,33 +11,50 @@ const dom = require("devtools/client/sha
 
 const AnimationList = createFactory(require("./AnimationList"));
 const AnimationListHeader = createFactory(require("./AnimationListHeader"));
 
 class AnimationListContainer extends PureComponent {
   static get propTypes() {
     return {
       animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+      emitEventForTest: PropTypes.func.isRequired,
+      getNodeFromActor: PropTypes.func.isRequired,
+      onHideBoxModelHighlighter: PropTypes.func.isRequired,
+      onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
+      setSelectedNode: PropTypes.func.isRequired,
     };
   }
 
   render() {
-    const { animations } = this.props;
+    const {
+      animations,
+      emitEventForTest,
+      getNodeFromActor,
+      onHideBoxModelHighlighter,
+      onShowBoxModelHighlighterForNode,
+      setSelectedNode,
+    } = this.props;
 
     return dom.div(
       {
         className: "animation-list-container"
       },
       AnimationListHeader(
         {
           animations
         }
       ),
       AnimationList(
         {
-          animations
+          animations,
+          emitEventForTest,
+          getNodeFromActor,
+          onHideBoxModelHighlighter,
+          onShowBoxModelHighlighterForNode,
+          setSelectedNode,
         }
       )
     );
   }
 }
 
 module.exports = AnimationListContainer;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/animation/components/AnimationTarget.js
@@ -0,0 +1,137 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this
+ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const { PureComponent } = require("devtools/client/shared/vendor/react");
+const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
+const dom = require("devtools/client/shared/vendor/react-dom-factories");
+
+const { REPS, MODE } = require("devtools/client/shared/components/reps/reps");
+const { Rep } = REPS;
+const ElementNode = REPS.ElementNode;
+
+class AnimationTarget extends PureComponent {
+  static get propTypes() {
+    return {
+      animation: PropTypes.object.isRequired,
+      emitEventForTest: PropTypes.func.isRequired,
+      getNodeFromActor: PropTypes.func.isRequired,
+      onHideBoxModelHighlighter: PropTypes.func.isRequired,
+      onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
+      setSelectedNode: PropTypes.func.isRequired,
+    };
+  }
+
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      nodeFront: null,
+    };
+  }
+
+  componentWillMount() {
+    this.updateNodeFront(this.props.animation);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (this.props.animation.actorID !== nextProps.animation.actorID) {
+      this.updateNodeFront(nextProps.animation);
+    }
+  }
+
+  shouldComponentUpdate(nextProps, nextState) {
+    return this.state.nodeFront !== nextState.nodeFront;
+  }
+
+  /**
+   * While waiting for a reps fix in https://github.com/devtools-html/reps/issues/92,
+   * translate nodeFront to a grip-like object that can be used with an ElementNode rep.
+   *
+   * @params  {NodeFront} nodeFront
+   *          The NodeFront for which we want to create a grip-like object.
+   * @returns {Object} a grip-like object that can be used with Reps.
+   */
+  translateNodeFrontToGrip(nodeFront) {
+    let { attributes } = nodeFront;
+
+    // The main difference between NodeFront and grips is that attributes are treated as
+    // a map in grips and as an array in NodeFronts.
+    let attributesMap = {};
+    for (let {name, value} of attributes) {
+      attributesMap[name] = value;
+    }
+
+    return {
+      actor: nodeFront.actorID,
+      preview: {
+        attributes: attributesMap,
+        attributesLength: attributes.length,
+        isConnected: true,
+        nodeName: nodeFront.nodeName.toLowerCase(),
+        nodeType: nodeFront.nodeType,
+      }
+    };
+  }
+
+  async updateNodeFront(animation) {
+    const { emitEventForTest, getNodeFromActor } = this.props;
+
+    // Try and get it from the playerFront directly.
+    let nodeFront = animation.animationTargetNodeFront;
+
+    // Next, get it from the walkerActor if it wasn't found.
+    if (!nodeFront) {
+      try {
+        nodeFront = await getNodeFromActor(animation.actorID);
+      } catch (e) {
+        // If an error occured while getting the nodeFront and if it can't be
+        // attributed to the panel having been destroyed in the meantime, this
+        // error needs to be logged and render needs to stop.
+        console.error(e);
+        return;
+      }
+    }
+
+    this.setState({ nodeFront });
+    emitEventForTest("animation-target-rendered");
+  }
+
+  render() {
+    const {
+      onHideBoxModelHighlighter,
+      onShowBoxModelHighlighterForNode,
+      setSelectedNode,
+    } = this.props;
+
+    const { nodeFront } = this.state;
+
+    if (!nodeFront) {
+      return dom.div(
+        {
+          className: "animation-target"
+        }
+      );
+    }
+
+    return dom.div(
+      {
+        className: "animation-target"
+      },
+      Rep(
+        {
+          defaultRep: ElementNode,
+          mode: MODE.TINY,
+          object: this.translateNodeFrontToGrip(nodeFront),
+          onDOMNodeMouseOut: () => onHideBoxModelHighlighter(),
+          onDOMNodeMouseOver: () => onShowBoxModelHighlighterForNode(nodeFront),
+          onInspectIconClick: () => setSelectedNode(nodeFront, "animation-panel"),
+        }
+      )
+    );
+  }
+}
+
+module.exports = AnimationTarget;
--- a/devtools/client/inspector/animation/components/App.js
+++ b/devtools/client/inspector/animation/components/App.js
@@ -11,35 +11,53 @@ const { connect } = require("devtools/cl
 
 const AnimationListContainer = createFactory(require("./AnimationListContainer"));
 const NoAnimationPanel = createFactory(require("./NoAnimationPanel"));
 
 class App extends PureComponent {
   static get propTypes() {
     return {
       animations: PropTypes.arrayOf(PropTypes.object).isRequired,
+      emitEventForTest: PropTypes.func.isRequired,
+      getNodeFromActor: PropTypes.func.isRequired,
+      onHideBoxModelHighlighter: PropTypes.func.isRequired,
+      onShowBoxModelHighlighterForNode: PropTypes.func.isRequired,
+      setSelectedNode: PropTypes.func.isRequired,
       toggleElementPicker: PropTypes.func.isRequired,
     };
   }
 
   shouldComponentUpdate(nextProps, nextState) {
     return this.props.animations.length !== 0 || nextProps.animations.length !== 0;
   }
 
   render() {
-    const { animations, toggleElementPicker } = this.props;
+    const {
+      animations,
+      emitEventForTest,
+      getNodeFromActor,
+      onHideBoxModelHighlighter,
+      onShowBoxModelHighlighterForNode,
+      setSelectedNode,
+      toggleElementPicker,
+    } = this.props;
 
     return dom.div(
       {
         id: "animation-container"
       },
       animations.length ?
       AnimationListContainer(
         {
-          animations
+          animations,
+          emitEventForTest,
+          getNodeFromActor,
+          onHideBoxModelHighlighter,
+          onShowBoxModelHighlighterForNode,
+          setSelectedNode,
         }
       )
       :
       NoAnimationPanel(
         {
           toggleElementPicker
         }
       )
--- a/devtools/client/inspector/animation/components/moz.build
+++ b/devtools/client/inspector/animation/components/moz.build
@@ -2,13 +2,14 @@
 # License, v. 2.0. If a copy of the MPL was not distributed with this
 # file, You can obtain one at http://mozilla.org/MPL/2.0/.
 
 DevToolsModules(
     'AnimationItem.js',
     'AnimationList.js',
     'AnimationListContainer.js',
     'AnimationListHeader.js',
+    'AnimationTarget.js',
     'AnimationTimelineTickItem.js',
     'AnimationTimelineTickList.js',
     'App.js',
-    'NoAnimationPanel.js'
+    'NoAnimationPanel.js',
 )
--- a/devtools/client/inspector/animation/test/head.js
+++ b/devtools/client/inspector/animation/test/head.js
@@ -31,16 +31,17 @@ registerCleanupFunction(() => {
  * Open the toolbox, with the inspector tool visible and the animationinspector
  * sidebar selected.
  *
  * @return {Promise} that resolves when the inspector is ready.
  */
 const openAnimationInspector = async function () {
   const { inspector, toolbox } = await openInspectorSidebarTab(TAB_NAME);
   await inspector.once("inspector-updated");
+  await waitForAllAnimationTargets(inspector);
   const { animationinspector: animationInspector } = inspector;
   const panel = inspector.panelWin.document.getElementById("animation-container");
   return { animationInspector, toolbox, inspector, panel };
 };
 
 /**
  * Close the toolbox.
  *
@@ -99,24 +100,39 @@ addTab = async function (url) {
  *                   and animations of its subtree are properly displayed.
  */
 const selectNodeAndWaitForAnimations = async function (data, inspector, reason = "test") {
   // We want to make sure the rest of the test waits for the animations to
   // be properly displayed (wait for all target DOM nodes to be previewed).
   const onUpdated = inspector.once("inspector-updated");
   await selectNode(data, inspector, reason);
   await onUpdated;
+  await waitForAllAnimationTargets(inspector);
 };
 
 /**
  * Set the sidebar width by given parameter.
  *
  * @param {String} width
  *        Change sidebar width by given parameter.
  * @param {InspectorPanel} inspector
  *        The instance of InspectorPanel currently loaded in the toolbox
  * @return {Promise} Resolves when the sidebar size changed.
  */
 const setSidebarWidth = async function (width, inspector) {
   const onUpdated = inspector.toolbox.once("inspector-sidebar-resized");
   inspector.splitBox.setState({ width });
   await onUpdated;
 };
+
+/**
+ * Wait for all AnimationTarget components to be fully loaded
+ * (fetched their related actor and rendered).
+ *
+ * @param {Inspector} inspector
+ */
+const waitForAllAnimationTargets = async function (inspector) {
+  const { animationinspector: animationInspector } = inspector;
+
+  for (let i = 0; i < animationInspector.animations.length; i++) {
+    await animationInspector.once("animation-target-rendered");
+  }
+};
--- a/devtools/client/inspector/animation/utils/utils.js
+++ b/devtools/client/inspector/animation/utils/utils.js
@@ -40,29 +40,33 @@ function findOptimalTimeInterval(minTime
       return interval;
     }
 
     multiplier *= 10;
   }
 }
 
 /**
- * Check the equality timing effects from given animations.
+ * Check the equality of the given animations.
  *
  * @param {Array} animations.
- * @param {Array} same to avobe.
- * @return {Boolean} true: same timing effects
+ * @param {Array} same to above.
+ * @return {Boolean} true: same animations
  */
-function isAllTimingEffectEqual(animationsA, animationsB) {
+function isAllAnimationEqual(animationsA, animationsB) {
   if (animationsA.length !== animationsB.length) {
     return false;
   }
 
   for (let i = 0; i < animationsA.length; i++) {
-    if (!isTimingEffectEqual(animationsA[i].state, animationsB[i].state)) {
+    const animationA = animationsA[i];
+    const animationB = animationsB[i];
+
+    if (animationA.actorID !== animationB.actorID ||
+        !isTimingEffectEqual(animationsA[i].state, animationsB[i].state)) {
       return false;
     }
   }
 
   return true;
 }
 
 /**
@@ -79,10 +83,10 @@ function isTimingEffectEqual(stateA, sta
          stateA.easing === stateB.easing &&
          stateA.endDelay === stateB.endDelay &&
          stateA.fill === stateB.fill &&
          stateA.iterationCount === stateB.iterationCount &&
          stateA.iterationStart === stateB.iterationStart;
 }
 
 exports.findOptimalTimeInterval = findOptimalTimeInterval;
-exports.isAllTimingEffectEqual = isAllTimingEffectEqual;
+exports.isAllAnimationEqual = isAllAnimationEqual;
 exports.isTimingEffectEqual = isTimingEffectEqual;
--- a/devtools/client/themes/animation.css
+++ b/devtools/client/themes/animation.css
@@ -2,16 +2,17 @@
  * License, v. 2.0. If a copy of the MPL was not distributed with this
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 /* Animation-inspector specific theme variables */
 
 :root {
   --animation-even-background-color: rgba(0, 0, 0, 0.05);
   --command-pick-image: url(chrome://devtools/skin/images/command-pick.svg);
+  --sidebar-width: 200px;
 }
 
 :root.theme-dark {
   --animation-even-background-color: rgba(255, 255, 255, 0.05);
 }
 
 :root.theme-firebug {
   --command-pick-image: url(chrome://devtools/skin/images/firebug/command-pick.svg);
@@ -23,17 +24,17 @@
   justify-content: flex-end;
   padding: 0;
 }
 
 /* Animation Timeline Tick List */
 .animation-timeline-tick-list {
   margin-right: 10px;
   position: relative;
-  width: calc(100% - 210px);
+  width: calc(100% - var(--sidebar-width) - 10px);
 }
 
 .animation-timeline-tick-item {
   border-left: 0.5px solid rgba(128, 136, 144, .5);
   height: 100vh;
   position: absolute;
 }
 
@@ -48,16 +49,29 @@
 .animation-item {
   height: 30px;
 }
 
 .animation-item:nth-child(2n+1) {
   background-color: var(--animation-even-background-color);
 }
 
+/* Animation Target */
+.animation-target {
+  align-items: center;
+  display: flex;
+  height: 100%;
+  padding-left: 4px;
+  width: var(--sidebar-width);
+}
+
+.animation-target .tag-name {
+  cursor: default;
+}
+
 /* No Animation Panel */
 .animation-error-message {
   overflow: auto;
 }
 
 .animation-error-message > p {
   white-space: pre;
 }