Bug 1226272 - Part 2: Implement saving and loading the tabs order preference. r?jdescottes draft
authorDaisuke Akatsuka <dakatsuka@mozilla.com>
Thu, 19 Apr 2018 18:46:55 +0900
changeset 784901 23d2ddeaf2f3510452cea715b5fd11cce2d86a6f
parent 784900 29b30610e52fa58c6c1f05455270524b75c2b0b5
child 784902 f208d2964ac621074d7f2d8b85fdb18f91d48804
push id107071
push userbmo:dakatsuka@mozilla.com
push dateThu, 19 Apr 2018 09:52:38 +0000
reviewersjdescottes
bugs1226272
milestone61.0a1
Bug 1226272 - Part 2: Implement saving and loading the tabs order preference. r?jdescottes MozReview-Commit-ID: JoVcnPwvVW7
devtools/client/framework/components/toolbox-tabs.js
devtools/client/framework/components/toolbox-toolbar.js
devtools/client/framework/toolbox-tabs-order-manager.js
devtools/client/framework/toolbox.js
devtools/client/preferences/devtools-client.js
--- a/devtools/client/framework/components/toolbox-tabs.js
+++ b/devtools/client/framework/components/toolbox-tabs.js
@@ -7,33 +7,34 @@ const { Component, createFactory } = req
 const dom = require("devtools/client/shared/vendor/react-dom-factories");
 const PropTypes = require("devtools/client/shared/vendor/react-prop-types");
 const {findDOMNode} = require("devtools/client/shared/vendor/react-dom");
 const {button, div} = dom;
 
 const Menu = require("devtools/client/framework/menu");
 const MenuItem = require("devtools/client/framework/menu-item");
 const ToolboxTab = createFactory(require("devtools/client/framework/components/toolbox-tab"));
-const ToolboxTabsOrderManager = require("devtools/client/framework/toolbox-tabs-order-manager");
+const { ToolboxTabsOrderManager } = require("devtools/client/framework/toolbox-tabs-order-manager");
 
 // 26px is chevron devtools button width.(i.e. tools-chevronmenu)
 const CHEVRON_BUTTON_WIDTH = 26;
 
 class ToolboxTabs extends Component {
   // See toolbox-toolbar propTypes for details on the props used here.
   static get propTypes() {
     return {
       currentToolId: PropTypes.string,
       focusButton: PropTypes.func,
       focusedButton: PropTypes.string,
       highlightedTools: PropTypes.object,
       panelDefinitions: PropTypes.array,
       selectTool: PropTypes.func,
       toolbox: PropTypes.object,
       L10N: PropTypes.object,
+      onTabsOrderUpdated: PropTypes.func.isRequired,
     };
   }
 
   constructor(props) {
     super(props);
 
     this.state = {
       // Array of overflowed tool id.
@@ -43,17 +44,17 @@ class ToolboxTabs extends Component {
     // Map with tool Id and its width size. This lifecycle is out of React's
     // lifecycle. If a tool is registered, ToolboxTabs will add target tool id
     // to this map. ToolboxTabs will never remove tool id from this cache.
     this._cachedToolTabsWidthMap = new Map();
 
     this._resizeTimerId = null;
     this.resizeHandler = this.resizeHandler.bind(this);
 
-    this._tabsOrderManager = new ToolboxTabsOrderManager();
+    this._tabsOrderManager = new ToolboxTabsOrderManager(props.onTabsOrderUpdated);
   }
 
   componentDidMount() {
     window.addEventListener("resize", this.resizeHandler);
     this.updateCachedToolTabsWidthMap();
     this.updateOverflowedTabs();
   }
 
--- a/devtools/client/framework/components/toolbox-toolbar.js
+++ b/devtools/client/framework/components/toolbox-toolbar.js
@@ -67,16 +67,18 @@ class ToolboxToolbar extends Component {
       focusButton: PropTypes.func,
       // Hold off displaying the toolbar until enough information is ready for
       // it to render nicely.
       canRender: PropTypes.bool,
       // Localization interface.
       L10N: PropTypes.object,
       // The devtools toolbox
       toolbox: PropTypes.object,
+      // Call back function to detect tabs order updated.
+      onTabsOrderUpdated: PropTypes.func.isRequired,
     };
   }
 
   /**
    * The render function is kept fairly short for maintainability. See the individual
    * render functions for how each of the sections is rendered.
    */
   render() {
--- a/devtools/client/framework/toolbox-tabs-order-manager.js
+++ b/devtools/client/framework/toolbox-tabs-order-manager.js
@@ -1,39 +1,48 @@
 /* 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 Services = require("Services");
+const PREFERENCE_NAME = "devtools.toolbox.tabsOrder";
+
 /**
  * Manage the order of devtools tabs.
  */
 class ToolboxTabsOrderManager {
-  constructor() {
+  constructor(onOrderUpdated) {
+    this.onOrderUpdated = onOrderUpdated;
+
     this.onMouseDown = this.onMouseDown.bind(this);
     this.onMouseMove = this.onMouseMove.bind(this);
     this.onMouseOut = this.onMouseOut.bind(this);
     this.onMouseUp = this.onMouseUp.bind(this);
+
+    Services.prefs.addObserver(PREFERENCE_NAME, this.onOrderUpdated);
   }
 
   destroy() {
+    Services.prefs.removeObserver(PREFERENCE_NAME, this.onOrderUpdated);
     this.onMouseUp();
   }
 
   onMouseDown(e) {
     if (!e.target.classList.contains("devtools-tab")) {
       return;
     }
 
     this.dragStartX = e.pageX;
     this.dragTarget = e.target;
     this.previousPageX = e.pageX;
     this.toolboxContainerElement = this.dragTarget.closest("#toolbox-container");
     this.toolboxTabsElement = this.dragTarget.closest(".toolbox-tabs");
+    this.isOrderUpdated = false;
 
     this.dragTarget.ownerDocument.addEventListener("mousemove", this.onMouseMove);
     this.dragTarget.ownerDocument.addEventListener("mouseout", this.onMouseOut);
     this.dragTarget.ownerDocument.addEventListener("mouseup", this.onMouseUp);
 
     this.toolboxContainerElement.classList.add("tabs-reordering");
   }
 
@@ -60,16 +69,18 @@ class ToolboxTabsOrderManager {
         if (isDragTargetPreviousSibling) {
           tabsElement.insertBefore(this.dragTarget, tabElement.nextSibling);
         } else {
           tabsElement.insertBefore(this.dragTarget, tabElement);
         }
 
         const xAfter = this.dragTarget.offsetLeft;
         this.dragStartX += xAfter - xBefore;
+
+        this.isOrderUpdated = true;
         break;
       }
     }
 
     let distance = e.pageX - this.dragStartX;
 
     if ((!this.dragTarget.previousSibling && distance < 0) ||
         (!this.dragTarget.nextSibling && distance > 0)) {
@@ -92,21 +103,53 @@ class ToolboxTabsOrderManager {
   onMouseUp() {
     if (!this.dragTarget) {
       // The case in here has two type:
       // 1. Although destroy method was called, it was not during reordering.
       // 2. Although mouse event occur, destroy method was called during reordering.
       return;
     }
 
+    if (this.isOrderUpdated) {
+      const ids =
+        [...this.toolboxTabsElement.querySelectorAll(".devtools-tab")]
+          .map(tabElement => tabElement.dataset.id);
+      const pref = ids.join(",");
+      Services.prefs.setCharPref(PREFERENCE_NAME, pref);
+    }
+
     this.dragTarget.ownerDocument.removeEventListener("mousemove", this.onMouseMove);
     this.dragTarget.ownerDocument.removeEventListener("mouseout", this.onMouseOut);
     this.dragTarget.ownerDocument.removeEventListener("mouseup", this.onMouseUp);
 
     this.toolboxContainerElement.classList.remove("tabs-reordering");
     this.dragTarget.style.left = null;
     this.dragTarget = null;
     this.toolboxContainerElement = null;
     this.toolboxTabsElement = null;
   }
 }
 
-module.exports = ToolboxTabsOrderManager;
+function sortPanelDefinitions(definitions) {
+  const pref = Services.prefs.getCharPref(PREFERENCE_NAME, "");
+
+  if (!pref) {
+    definitions.sort(definition => {
+      return -1 * (definition.ordinal == undefined || definition.ordinal < 0
+        ? Number.MAX_VALUE
+        : definition.ordinal
+      );
+    });
+  }
+
+  const toolIds = pref.split(",");
+
+  return definitions.sort((a, b) => {
+    let orderA = toolIds.indexOf(a.id);
+    let orderB = toolIds.indexOf(b.id);
+    orderA = orderA < 0 ? Number.MAX_VALUE : orderA;
+    orderB = orderB < 0 ? Number.MAX_VALUE : orderB;
+    return orderA - orderB;
+  });
+}
+
+module.exports.ToolboxTabsOrderManager = ToolboxTabsOrderManager;
+module.exports.sortPanelDefinitions = sortPanelDefinitions;
--- a/devtools/client/framework/toolbox.js
+++ b/devtools/client/framework/toolbox.js
@@ -71,16 +71,18 @@ loader.lazyRequireGetter(this, "viewSour
 loader.lazyRequireGetter(this, "StyleSheetsFront",
   "devtools/shared/fronts/stylesheets", true);
 loader.lazyRequireGetter(this, "buildHarLog",
   "devtools/client/netmonitor/src/har/har-builder-utils", true);
 loader.lazyRequireGetter(this, "getKnownDeviceFront",
   "devtools/shared/fronts/device", true);
 loader.lazyRequireGetter(this, "NetMonitorAPI",
   "devtools/client/netmonitor/src/api", true);
+loader.lazyRequireGetter(this, "sortPanelDefinitions",
+  "devtools/client/framework/toolbox-tabs-order-manager", true);
 
 loader.lazyGetter(this, "domNodeConstants", () => {
   return require("devtools/shared/dom-node-constants");
 });
 
 loader.lazyGetter(this, "registerHarOverlay", () => {
   return require("devtools/client/netmonitor/src/har/toolbox-overlay").register;
 });
@@ -142,16 +144,17 @@ function Toolbox(target, selectedTool, h
   this._applyServiceWorkersTestingSettings =
     this._applyServiceWorkersTestingSettings.bind(this);
   this._saveSplitConsoleHeight = this._saveSplitConsoleHeight.bind(this);
   this._onFocus = this._onFocus.bind(this);
   this._onBrowserMessage = this._onBrowserMessage.bind(this);
   this._showDevEditionPromo = this._showDevEditionPromo.bind(this);
   this._updateTextBoxMenuItems = this._updateTextBoxMenuItems.bind(this);
   this._onPerformanceFrontEvent = this._onPerformanceFrontEvent.bind(this);
+  this._onTabsOrderUpdated = this._onTabsOrderUpdated.bind(this);
   this._onToolbarFocus = this._onToolbarFocus.bind(this);
   this._onToolbarArrowKeypress = this._onToolbarArrowKeypress.bind(this);
   this._onPickerClick = this._onPickerClick.bind(this);
   this._onPickerKeypress = this._onPickerKeypress.bind(this);
   this._onPickerStarted = this._onPickerStarted.bind(this);
   this._onPickerStopped = this._onPickerStopped.bind(this);
   this._onInspectObject = this._onInspectObject.bind(this);
   this._onNewSelectedNodeFront = this._onNewSelectedNodeFront.bind(this);
@@ -258,23 +261,18 @@ Toolbox.prototype = {
     }
   },
 
   /**
    * Combines the built-in panel definitions and the additional tool definitions that
    * can be set by add-ons.
    */
   _combineAndSortPanelDefinitions() {
-    const definitions = [...this._panelDefinitions, ...this.getVisibleAdditionalTools()];
-    definitions.sort(definition => {
-      return -1 * (definition.ordinal == undefined || definition.ordinal < 0
-        ? MAX_ORDINAL
-        : definition.ordinal
-      );
-    });
+    let definitions = [...this._panelDefinitions, ...this.getVisibleAdditionalTools()];
+    definitions = sortPanelDefinitions(definitions);
     this.component.setPanelDefinitions(definitions);
   },
 
   lastUsedToolId: null,
 
   /**
    * Returns a *copy* of the _toolPanels collection.
    *
@@ -1144,17 +1142,18 @@ Toolbox.prototype = {
     const element = this.React.createElement(this.ToolboxController, {
       L10N,
       currentToolId: this.currentToolId,
       selectTool: this.selectTool,
       toggleSplitConsole: this.toggleSplitConsole,
       toggleNoAutohide: this.toggleNoAutohide,
       closeToolbox: this.destroy,
       focusButton: this._onToolbarFocus,
-      toolbox: this
+      toolbox: this,
+      onTabsOrderUpdated: this._onTabsOrderUpdated,
     });
 
     this.component = this.ReactDOM.render(element, this._componentMount);
   },
 
   /**
    * Reset tabindex attributes across all focusable elements inside the toolbar.
    * Only have one element with tabindex=0 at a time to make sure that tabbing
@@ -1910,16 +1909,20 @@ Toolbox.prototype = {
     if (originalTarget.nodeType !== 1 ||
         originalTarget.baseURI === webconsoleURL) {
       return;
     }
 
     this._lastFocusedElement = originalTarget;
   },
 
+  _onTabsOrderUpdated: function() {
+    this._combineAndSortPanelDefinitions();
+  },
+
   /**
    * Opens the split console.
    *
    * @returns {Promise} a promise that resolves once the tool has been
    *          loaded and focused.
    */
   openSplitConsole: function() {
     this._splitConsole = true;
--- a/devtools/client/preferences/devtools-client.js
+++ b/devtools/client/preferences/devtools-client.js
@@ -24,16 +24,17 @@ pref("devtools.toolbox.footer.height", 2
 pref("devtools.toolbox.sidebar.width", 500);
 pref("devtools.toolbox.host", "bottom");
 pref("devtools.toolbox.previousHost", "side");
 pref("devtools.toolbox.selectedTool", "webconsole");
 pref("devtools.toolbox.sideEnabled", true);
 pref("devtools.toolbox.zoomValue", "1");
 pref("devtools.toolbox.splitconsoleEnabled", false);
 pref("devtools.toolbox.splitconsoleHeight", 100);
+pref("devtools.toolbox.tabsOrder", "");
 
 // Toolbox Button preferences
 pref("devtools.command-button-pick.enabled", true);
 pref("devtools.command-button-frames.enabled", true);
 pref("devtools.command-button-splitconsole.enabled", true);
 pref("devtools.command-button-paintflashing.enabled", false);
 pref("devtools.command-button-scratchpad.enabled", false);
 pref("devtools.command-button-responsive.enabled", true);