Bug 1443846 - Add skeleton for font editor panel. r=gl draft
authorRazvan Caliman <rcaliman@mozilla.com>
Tue, 13 Mar 2018 21:15:52 +0100
changeset 770994 40d7c30665cce03f9567553a64b4538af846214a
parent 770993 df67a8ef838e685fd23eb9054e40eece6653fd0c
child 770995 fad8e1f165d8ce4a4a69833dd8f4d5a66d0ae82a
push id103549
push userbmo:rcaliman@mozilla.com
push dateThu, 22 Mar 2018 08:29:21 +0000
reviewersgl
bugs1443846
milestone61.0a1
Bug 1443846 - Add skeleton for font editor panel. r=gl - Implement basic React component & Redux store and actions for font editor. - Move font overview rendering from FontsApp into its own component: FontOverview. FontsApp remains just a wrapper for FontEditor and FontOverview. - Listen to rule selection events to toggle the display of the font editor and font overview panels. MozReview-Commit-ID: 496LHPqpnKL
devtools/client/inspector/fonts/actions/font-editor.js
devtools/client/inspector/fonts/actions/index.js
devtools/client/inspector/fonts/actions/moz.build
devtools/client/inspector/fonts/components/FontEditor.js
devtools/client/inspector/fonts/components/FontOverview.js
devtools/client/inspector/fonts/components/FontsApp.js
devtools/client/inspector/fonts/components/moz.build
devtools/client/inspector/fonts/fonts.js
devtools/client/inspector/fonts/reducers/font-editor.js
devtools/client/inspector/fonts/reducers/moz.build
devtools/client/inspector/fonts/types.js
devtools/client/inspector/reducers.js
devtools/client/inspector/rules/rules.js
devtools/client/themes/fonts.css
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/actions/font-editor.js
@@ -0,0 +1,21 @@
+/* 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 {
+  UPDATE_EDITOR_VISIBILITY,
+} = require("./index");
+
+module.exports = {
+
+  toggleFontEditor(isVisible, selector) {
+    return {
+      type: UPDATE_EDITOR_VISIBILITY,
+      isVisible,
+      selector,
+    };
+  },
+
+};
--- a/devtools/client/inspector/fonts/actions/index.js
+++ b/devtools/client/inspector/fonts/actions/index.js
@@ -3,15 +3,18 @@
  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
 
 "use strict";
 
 const { createEnum } = require("devtools/client/shared/enum");
 
 createEnum([
 
+  // Toggle the visibiltiy of the font editor
+  "UPDATE_EDITOR_VISIBILITY",
+
   // Update the list of fonts.
   "UPDATE_FONTS",
 
   // Update the preview text.
   "UPDATE_PREVIEW_TEXT",
 
 ], module.exports);
--- a/devtools/client/inspector/fonts/actions/moz.build
+++ b/devtools/client/inspector/fonts/actions/moz.build
@@ -1,11 +1,12 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
+    'font-editor.js',
     'font-options.js',
     'fonts.js',
     'index.js',
 )
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/components/FontEditor.js
@@ -0,0 +1,31 @@
+/* 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 Types = require("../types");
+
+class FontEditor extends PureComponent {
+  static get propTypes() {
+    return {
+      fontEditor: PropTypes.shape(Types.fontEditor).isRequired,
+    };
+  }
+
+  render() {
+    const { selector } = this.props.fontEditor;
+
+    return dom.div(
+      {
+        className: "theme-sidebar inspector-tabpanel",
+        id: "sidebar-panel-fonteditor"
+      }, `Placeholder for Font Editor panel for selector: ${selector}`
+    );
+  }
+}
+
+module.exports = FontEditor;
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/components/FontOverview.js
@@ -0,0 +1,88 @@
+/* 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 { 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 Accordion = createFactory(require("devtools/client/inspector/layout/components/Accordion"));
+const FontList = createFactory(require("./FontList"));
+
+const { getStr } = require("../utils/l10n");
+const Types = require("../types");
+
+class FontOverview extends PureComponent {
+  static get propTypes() {
+    return {
+      fontData: PropTypes.shape(Types.fontData).isRequired,
+      fontOptions: PropTypes.shape(Types.fontOptions).isRequired,
+      onPreviewFonts: PropTypes.func.isRequired,
+    };
+  }
+
+  renderElementFonts() {
+    let {
+      fontData,
+      fontOptions,
+      onPreviewFonts,
+    } = this.props;
+    let { fonts } = fontData;
+
+    return fonts.length ?
+      FontList({
+        fonts,
+        fontOptions,
+        onPreviewFonts
+      })
+      :
+      dom.div(
+        {
+          className: "devtools-sidepanel-no-result"
+        },
+        getStr("fontinspector.noFontsOnSelectedElement")
+      );
+  }
+
+  renderOtherFonts() {
+    let {
+      fontData,
+      onPreviewFonts,
+      fontOptions,
+    } = this.props;
+    let { otherFonts } = fontData;
+
+    if (!otherFonts.length) {
+      return null;
+    }
+
+    return Accordion({
+      items: [
+        {
+          header: getStr("fontinspector.otherFontsInPageHeader"),
+          component: FontList,
+          componentProps: {
+            fontOptions,
+            fonts: otherFonts,
+            onPreviewFonts
+          },
+          opened: false
+        }
+      ]
+    });
+  }
+
+  render() {
+    return dom.div(
+      {
+        id: "font-container",
+      },
+      this.renderElementFonts(),
+      this.renderOtherFonts()
+    );
+  }
+}
+
+module.exports = FontOverview;
--- a/devtools/client/inspector/fonts/components/FontsApp.js
+++ b/devtools/client/inspector/fonts/components/FontsApp.js
@@ -4,92 +4,51 @@
 
 "use strict";
 
 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 { connect } = require("devtools/client/shared/vendor/react-redux");
 
-const Accordion = createFactory(require("devtools/client/inspector/layout/components/Accordion"));
-const FontList = createFactory(require("./FontList"));
+const FontEditor = createFactory(require("./FontEditor"));
+const FontOverview = createFactory(require("./FontOverview"));
 
-const { getStr } = require("../utils/l10n");
 const Types = require("../types");
 
 class FontsApp extends PureComponent {
   static get propTypes() {
     return {
       fontData: PropTypes.shape(Types.fontData).isRequired,
+      fontEditor: PropTypes.shape(Types.fontEditor).isRequired,
       fontOptions: PropTypes.shape(Types.fontOptions).isRequired,
       onPreviewFonts: PropTypes.func.isRequired,
     };
   }
 
-  renderElementFonts() {
-    let {
-      fontData,
-      fontOptions,
-      onPreviewFonts,
-    } = this.props;
-    let { fonts } = fontData;
-
-    return fonts.length ?
-      FontList({
-        fonts,
-        fontOptions,
-        onPreviewFonts
-      })
-      :
-      dom.div(
-        {
-          className: "devtools-sidepanel-no-result"
-        },
-        getStr("fontinspector.noFontsOnSelectedElement")
-      );
-  }
-
-  renderOtherFonts() {
-    let {
+  render() {
+    const {
       fontData,
-      onPreviewFonts,
+      fontEditor,
       fontOptions,
+      onPreviewFonts
     } = this.props;
-    let { otherFonts } = fontData;
-
-    if (!otherFonts.length) {
-      return null;
-    }
 
-    return Accordion({
-      items: [
-        {
-          header: getStr("fontinspector.otherFontsInPageHeader"),
-          component: FontList,
-          componentProps: {
-            fontOptions,
-            fonts: otherFonts,
-            onPreviewFonts
-          },
-          opened: false
-        }
-      ]
-    });
-  }
-
-  render() {
     return dom.div(
       {
         className: "theme-sidebar inspector-tabpanel",
         id: "sidebar-panel-fontinspector"
       },
-      dom.div(
-        {
-          id: "font-container"
-        },
-        this.renderElementFonts(),
-        this.renderOtherFonts()
-      )
+      this.props.fontEditor.isVisible ?
+        FontEditor({
+          fontEditor,
+        })
+        :
+        FontOverview({
+          fontData,
+          fontOptions,
+          onPreviewFonts,
+        })
     );
   }
 }
 
 module.exports = connect(state => state)(FontsApp);
--- a/devtools/client/inspector/fonts/components/moz.build
+++ b/devtools/client/inspector/fonts/components/moz.build
@@ -1,12 +1,14 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
     'Font.js',
+    'FontEditor.js',
     'FontList.js',
+    'FontOverview.js',
     'FontPreview.js',
     'FontsApp.js',
 )
--- a/devtools/client/inspector/fonts/fonts.js
+++ b/devtools/client/inspector/fonts/fonts.js
@@ -14,28 +14,34 @@ const { Provider } = require("devtools/c
 const FontsApp = createFactory(require("./components/FontsApp"));
 
 const { LocalizationHelper } = require("devtools/shared/l10n");
 const INSPECTOR_L10N =
   new LocalizationHelper("devtools/client/locales/inspector.properties");
 
 const { updateFonts } = require("./actions/fonts");
 const { updatePreviewText } = require("./actions/font-options");
+const { toggleFontEditor } = require("./actions/font-editor");
+
+const FONT_EDITOR_ID = "fonteditor";
 
 class FontInspector {
   constructor(inspector, window) {
     this.document = window.document;
     this.inspector = inspector;
     this.pageStyle = this.inspector.pageStyle;
+    this.ruleView = this.inspector.getPanel("ruleview").view;
+    this.selectedRule = null;
     this.store = this.inspector.store;
 
     this.update = this.update.bind(this);
-
     this.onNewNode = this.onNewNode.bind(this);
     this.onPreviewFonts = this.onPreviewFonts.bind(this);
+    this.onRuleSelected = this.onRuleSelected.bind(this);
+    this.onRuleUnselected = this.onRuleUnselected.bind(this);
     this.onThemeChanged = this.onThemeChanged.bind(this);
 
     this.init();
   }
 
   init() {
     if (!this.inspector) {
       return;
@@ -52,16 +58,18 @@ class FontInspector {
       title: INSPECTOR_L10N.getStr("inspector.sidebar.fontInspectorTitle"),
     }, fontsApp);
 
     // Expose the provider to let inspector.js use it in setupSidebar.
     this.provider = provider;
 
     this.inspector.selection.on("new-node-front", this.onNewNode);
     this.inspector.sidebar.on("fontinspector-selected", this.onNewNode);
+    this.ruleView.on("ruleview-rule-selected", this.onRuleSelected);
+    this.ruleView.on("ruleview-rule-unselected", this.onRuleUnselected);
 
     // Listen for theme changes as the color of the previews depend on the theme
     gDevTools.on("theme-switched", this.onThemeChanged);
 
     this.store.dispatch(updatePreviewText(""));
     this.update(false, "");
   }
 
@@ -84,21 +92,25 @@ class FontInspector {
 
   /**
    * Destruction function called when the inspector is destroyed. Removes event listeners
    * and cleans up references.
    */
   destroy() {
     this.inspector.selection.off("new-node-front", this.onNewNode);
     this.inspector.sidebar.off("fontinspector-selected", this.onNewNode);
+    this.ruleView.off("ruleview-rule-selected", this.onRuleSelected);
+    this.ruleView.off("ruleview-rule-unselected", this.onRuleUnselected);
     gDevTools.off("theme-switched", this.onThemeChanged);
 
     this.document = null;
     this.inspector = null;
     this.pageStyle = null;
+    this.ruleView = null;
+    this.selectedRule = null;
     this.store = null;
   }
 
   async getFontsForNode(node, options) {
     // In case we've been destroyed in the meantime
     if (!this.document) {
       return [];
     }
@@ -146,16 +158,55 @@ class FontInspector {
    * Handler for change in preview input.
    */
   onPreviewFonts(value) {
     this.store.dispatch(updatePreviewText(value));
     this.update();
   }
 
   /**
+   * Handler for "ruleview-rule-selected" event emitted from the rule view when a rule is
+   * marked as selected for an editor.
+   * If selected for the font editor, hold a reference to the rule so we know where to
+   * put property changes coming from the font editor and show the font editor panel.
+   *
+   * @param {Object} eventData
+   *        Data payload for the event. Contains:
+   *        - {String} editorId - id of the editor for which the rule was selected
+   *        - {Rule} rule - reference to rule that was selected
+   */
+  onRuleSelected(eventData) {
+    const { editorId, rule } = eventData;
+    if (editorId === FONT_EDITOR_ID) {
+      const selector = rule.matchedSelectors[0];
+      this.selectedRule = rule;
+      this.store.dispatch(toggleFontEditor(true, selector));
+    }
+  }
+
+  /**
+   * Handler for "ruleview-rule-unselected" event emitted from the rule view when a rule
+   * was released from being selected for an editor.
+   * If previously selected for the font editor, release the reference to the rule and
+   * hide the font editor panel.
+   *
+   * @param {Object} eventData
+   *        Data payload for the event. Contains:
+   *        - {String} editorId - id of the editor for which the rule was released
+   *        - {Rule} rule - reference to rule that was released
+   */
+  onRuleUnselected(eventData) {
+    const { editorId, rule } = eventData;
+    if (editorId === FONT_EDITOR_ID && rule == this.selectedRule) {
+      this.selectedRule = null;
+      this.store.dispatch(toggleFontEditor(false));
+    }
+  }
+
+  /**
    * Handler for the "theme-switched" event.
    */
   onThemeChanged(frame) {
     if (frame === this.document.defaultView) {
       this.update();
     }
   }
 
new file mode 100644
--- /dev/null
+++ b/devtools/client/inspector/fonts/reducers/font-editor.js
@@ -0,0 +1,33 @@
+/* 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 {
+  UPDATE_EDITOR_VISIBILITY,
+} = require("../actions/index");
+
+const INITIAL_STATE = {
+  // Whether or not the font editor is visible.
+  isVisible: false,
+  // Selector text of the rule where font properties will be written.
+  selector: "",
+};
+
+let reducers = {
+
+  [UPDATE_EDITOR_VISIBILITY](state, { isVisible, selector }) {
+    selector = isVisible ? selector : "";
+    return { ...state, isVisible, selector };
+  },
+
+};
+
+module.exports = function(state = INITIAL_STATE, action) {
+  let reducer = reducers[action.type];
+  if (!reducer) {
+    return state;
+  }
+  return reducer(state, action);
+};
--- a/devtools/client/inspector/fonts/reducers/moz.build
+++ b/devtools/client/inspector/fonts/reducers/moz.build
@@ -1,10 +1,11 @@
 # -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
 # vim: set filetype=python:
 # 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/.
 
 DevToolsModules(
+    'font-editor.js',
     'font-options.js',
     'fonts.js',
 )
--- a/devtools/client/inspector/fonts/types.js
+++ b/devtools/client/inspector/fonts/types.js
@@ -74,16 +74,27 @@ const font = exports.font = {
   variationInstances: PropTypes.arrayOf(PropTypes.shape(fontVariationInstance))
 };
 
 exports.fontOptions = {
   // The current preview text
   previewText: PropTypes.string,
 };
 
+exports.fontEditor = {
+  // Font currently being edited
+  font: PropTypes.shape(font),
+
+  // Whether or not the font editor is visible
+  isVisible: PropTypes.bool,
+
+  // Selector text of the rule where font properties will be written
+  selector: PropTypes.string,
+};
+
 /**
  * Font data.
  */
 exports.fontData = {
   // The fonts used in the current element.
   fonts: PropTypes.arrayOf(PropTypes.shape(font)),
 
   // Fonts used elsewhere.
--- a/devtools/client/inspector/reducers.js
+++ b/devtools/client/inspector/reducers.js
@@ -10,10 +10,11 @@
 exports.animations = require("devtools/client/inspector/animation/reducers/animations");
 exports.boxModel = require("devtools/client/inspector/boxmodel/reducers/box-model");
 exports.changes = require("devtools/client/inspector/changes/reducers/changes");
 exports.events = require("devtools/client/inspector/events/reducers/events");
 exports.extensionsSidebar = require("devtools/client/inspector/extensions/reducers/sidebar");
 exports.flexbox = require("devtools/client/inspector/flexbox/reducers/flexbox");
 exports.fontOptions = require("devtools/client/inspector/fonts/reducers/font-options");
 exports.fontData = require("devtools/client/inspector/fonts/reducers/fonts");
+exports.fontEditor = require("devtools/client/inspector/fonts/reducers/font-editor");
 exports.grids = require("devtools/client/inspector/grids/reducers/grids");
 exports.highlighterSettings = require("devtools/client/inspector/grids/reducers/highlighter-settings");
--- a/devtools/client/inspector/rules/rules.js
+++ b/devtools/client/inspector/rules/rules.js
@@ -105,17 +105,17 @@ function CssRuleView(inspector, document
 
   this.inspector = inspector;
   this.highlighters = inspector.highlighters;
   this.styleDocument = document;
   this.styleWindow = this.styleDocument.defaultView;
   this.store = store || {};
   // References to rules marked by various editors where they intend to write changes.
   // @see selectRule(), unselectRule()
-  this.selectedRules = {};
+  this.selectedRules = new Map();
   this.pageStyle = pageStyle;
 
   // Allow tests to override debouncing behavior, as this can cause intermittents.
   this.debounce = debounce;
 
   this.cssProperties = getCssProperties(inspector.toolbox);
 
   this._outputParser = new OutputParser(document, this.cssProperties);
@@ -1221,80 +1221,84 @@ CssRuleView.prototype = {
   *        Key to use for collecting references to selected rules.
   * @param {Boolean} [unselectOthers=true]
   *        Optional. Default: `true`. If true, unselect all other rules that were
   *        selected for the given editor. Ensures only one rule at a time is selected for
   *        a particular editor. Set to `false` if an editor may operate on multiple rules
   *        at a time.
   */
   selectRule(rule, editorId, unselectOthers = true) {
-    this.selectedRules[editorId] = this.getSelectedRules(editorId);
-
-    if (!this.selectedRules[editorId].includes(rule)) {
-      this.selectedRules[editorId].push(rule);
+    const rules = this.getSelectedRules(editorId);
+    if (!rules.includes(rule)) {
+      this.selectedRules.set(editorId, [...rules, rule]);
     }
 
     // Mark other rules for this editorId as unselected.
     if (unselectOthers) {
-      this.selectedRules[editorId]
+      rules
         .filter(item => item !== rule)
         .map(item => this.unselectRule(item, editorId));
     }
 
     this.emit("ruleview-rule-selected", {editorId, rule});
   },
 
   /**
    * Unmark a rule as selected for the given editor id.
    *
    * @param {Rule} rule
    *        Rule object for which to remove the reference.
    * @param {String} editorId
    *        Key for which to mark the given rule as selected.
    */
   unselectRule(rule, editorId) {
-    if (!Array.isArray(this.selectedRules[editorId])) {
+    const rules = this.selectedRules.get(editorId);
+    if (!Array.isArray(rules)) {
       return;
     }
 
-    let index = this.selectedRules[editorId].findIndex(item => item === rule);
+    let index = rules.findIndex(item => item === rule);
     if (index === -1) {
       return;
     }
 
-    this.selectedRules[editorId].splice(index, 1);
+    rules.splice(index, 1);
+    this.selectedRules.set(editorId, rules);
     this.emit("ruleview-rule-unselected", {editorId, rule});
   },
 
   /**
   * Unmark all selected rules for all editors. If an editor id is provided, unmark all
   * selected rules just for that editor leaving others untouched.
   *
-  * @param {String} [editorId]
+  * @param {String} editorId
   *        Optional editor id for which to restrict unselect operation.
   */
   unselectAllRules(editorId) {
-    let keys = Object.keys(this.selectedRules);
-    keys = editorId ? keys.filter(key => (key === editorId)) : keys;
-    for (let key of keys) {
-      this.selectedRules[key].map(item => this.unselectRule(item, key));
+    for (let [id, rules] of this.selectedRules) {
+      // If we're supposed to unselect rules from just one editorId but it did not match,
+      // skip this iteration.
+      if (editorId && id !== editorId) {
+        continue;
+      }
+      rules.map(rule => this.unselectRule(rule, id));
     }
   },
 
   /**
    * Return an array of selected rules for the given editor id.
    * If no rules match, return an empty arrary;
    *
    * @param {String} editorId
    *        Editor id for which to return selected rules.
    * @return {Array}
    */
   getSelectedRules(editorId) {
-    return Array.isArray(this.selectedRules[editorId]) ?
-      this.selectedRules[editorId] : [];
+    const rules = this.selectedRules.get(editorId);
+    return Array.isArray(rules) ? rules : [];
   },
 
   /**
    * Called when a rule from the Rule view was marked as selected for an editor.
    * Handle the event and show panels relevant for the given editor id.
    *
    * @param {Object} eventData
    *        Data payload for the event. Contains:
--- a/devtools/client/themes/fonts.css
+++ b/devtools/client/themes/fonts.css
@@ -5,16 +5,20 @@
 #sidebar-panel-fontinspector {
   margin: 0;
   display: flex;
   flex-direction: column;
   width: 100%;
   height: 100%;
 }
 
+#sidebar-panel-fonteditor {
+  padding: 1em;
+}
+
 #font-container {
   overflow: auto;
   flex: auto;
 }
 
 .fonts-list {
   padding: 0;
   margin: 0;