Bug 1023304 - auto-add new builtin widgets in customizable areas, r=mconley
authorGijs Kruitbosch <gijskruitbosch@gmail.com>
Wed, 09 Jul 2014 23:57:19 +0100
changeset 215716 2cfd5b1c4759a49c5c0abe757f3b3beacb3c4e97
parent 215715 4565c08e0027753473e542cec142949f6cbb9c79
child 215717 ee57ae5dc80657d08337e00dc30a1164c52c3137
push id515
push userraliiev@mozilla.com
push dateMon, 06 Oct 2014 12:51:51 +0000
treeherdermozilla-release@267c7a481bef [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersmconley
bugs1023304
milestone33.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 1023304 - auto-add new builtin widgets in customizable areas, r=mconley
browser/components/customizableui/src/CustomizableUI.jsm
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -44,16 +44,21 @@ const kPrefDrawInTitlebar            = "
  * of providing onViewShowing and onViewHiding event handlers.
  */
 const kSubviewEvents = [
   "ViewShowing",
   "ViewHiding"
 ];
 
 /**
+ * The current version. We can use this to auto-add new default widgets as necessary.
+ */
+const kVersion = 0;
+
+/**
  * gPalette is a map of every widget that CustomizableUI.jsm knows about, keyed
  * on their IDs.
  */
 let gPalette = new Map();
 
 /**
  * gAreas maps area IDs to Sets of properties about those areas. An area is a
  * place where a widget can be put.
@@ -140,16 +145,17 @@ let gModuleName = "[CustomizableUI]";
 
 let CustomizableUIInternal = {
   initialize: function() {
     LOG("Initializing");
 
     this.addListener(this);
     this._defineBuiltInWidgets();
     this.loadSavedState();
+    this._introduceNewBuiltinWidgets();
 
     let panelPlacements = [
       "edit-controls",
       "zoom-controls",
       "new-window-button",
       "privatebrowsing-button",
       "save-page-button",
       "print-button",
@@ -271,16 +277,35 @@ let CustomizableUIInternal = {
   _defineBuiltInWidgets: function() {
     //XXXunf Need to figure out how to auto-add new builtin widgets in new
     //       app versions to already customized areas.
     for (let widgetDefinition of CustomizableWidgets) {
       this.createBuiltinWidget(widgetDefinition);
     }
   },
 
+  _introduceNewBuiltinWidgets: function() {
+    if (!gSavedState || gSavedState.currentVersion >= kVersion) {
+      return;
+    }
+
+    let currentVersion = gSavedState.currentVersion;
+    for (let [id, widget] of gPalette) {
+      if (widget._introducedInVersion > currentVersion &&
+          widget.defaultArea) {
+        let futurePlacements = gFuturePlacements.get(widget.defaultArea);
+        if (futurePlacements) {
+          futurePlacements.add(id);
+        } else {
+          gFuturePlacements.set(widget.defaultArea, new Set([id]));
+        }
+      }
+    }
+  },
+
   wrapWidget: function(aWidgetId) {
     if (gGroupWrapperCache.has(aWidgetId)) {
       return gGroupWrapperCache.get(aWidgetId);
     }
 
     let provider = this.getWidgetProvider(aWidgetId);
     if (!provider) {
       return null;
@@ -354,17 +379,19 @@ let CustomizableUIInternal = {
     }
 
     if (!areaIsKnown) {
       gAreas.set(aName, props);
 
       if (props.get("legacy") && !gPlacements.has(aName)) {
         // Guarantee this area exists in gFuturePlacements, to avoid checking it in
         // various places elsewhere.
-        gFuturePlacements.set(aName, new Set());
+        if (!gFuturePlacements.has(aName)) {
+          gFuturePlacements.set(aName, new Set());
+        }
       } else {
         this.restoreStateForArea(aName);
       }
 
       // If we have pending build area nodes, register all of them
       if (gPendingBuildAreas.has(aName)) {
         let pendingNodes = gPendingBuildAreas.get(aName);
         for (let [pendingNode, existingChildren] of pendingNodes) {
@@ -1741,16 +1768,20 @@ let CustomizableUIInternal = {
       gSavedState = {};
       LOG("Error loading saved UI customization state, falling back to defaults.");
     }
 
     if (!("placements" in gSavedState)) {
       gSavedState.placements = {};
     }
 
+    if (!("currentVersion" in gSavedState)) {
+      gSavedState.currentVersion = 0;
+    }
+
     gSeenWidgets = new Set(gSavedState.seen || []);
     gDirtyAreaCache = new Set(gSavedState.dirtyAreaCache || []);
     gNewElementCount = gSavedState.newElementCount || 0;
   },
 
   restoreStateForArea: function(aArea, aLegacyState) {
     let placementsPreexisted = gPlacements.has(aArea);
 
@@ -1799,16 +1830,17 @@ let CustomizableUIInternal = {
       }
 
       // Finally, add widgets to the area that were added before the it was able
       // to be restored. This can occur when add-ons register widgets for a
       // lazily-restored area before it's been restored.
       if (gFuturePlacements.has(aArea)) {
         for (let id of gFuturePlacements.get(aArea))
           this.addWidgetToArea(id, aArea);
+        gFuturePlacements.delete(aArea);
       }
 
       LOG("Placements for " + aArea + ":\n\t" + gPlacements.get(aArea).join("\n\t"));
 
       gRestoring = false;
     } finally {
       this.endBatchUpdate();
     }
@@ -1816,16 +1848,17 @@ let CustomizableUIInternal = {
 
   saveState: function() {
     if (gInBatchStack || !gDirty) {
       return;
     }
     let state = { placements: gPlacements,
                   seen: gSeenWidgets,
                   dirtyAreaCache: gDirtyAreaCache,
+                  currentVersion: kVersion,
                   newElementCount: gNewElementCount };
 
     LOG("Saving state.");
     let serialized = JSON.stringify(state, this.serializerHelper);
     LOG("State saved as: " + serialized);
     Services.prefs.setCharPref(kPrefCustomizationState, serialized);
     gDirty = false;
   },
@@ -1925,20 +1958,18 @@ let CustomizableUIInternal = {
       }
     }
 
     this.notifyListeners("onWidgetCreated", widget.id);
 
     if (widget.defaultArea) {
       let addToDefaultPlacements = false;
       let area = gAreas.get(widget.defaultArea);
-      if (widget.source == CustomizableUI.SOURCE_BUILTIN) {
-        addToDefaultPlacements = true;
-      } else if (!CustomizableUI.isBuiltinToolbar(widget.defaultArea) &&
-                 widget.defaultArea != CustomizableUI.AREA_PANEL) {
+      if (!CustomizableUI.isBuiltinToolbar(widget.defaultArea) &&
+          widget.defaultArea != CustomizableUI.AREA_PANEL) {
         addToDefaultPlacements = true;
       }
 
       if (addToDefaultPlacements) {
         if (area.has("defaultPlacements")) {
           area.get("defaultPlacements").push(widget.id);
         } else {
           area.set("defaultPlacements", [widget.id]);
@@ -2055,16 +2086,17 @@ let CustomizableUIInternal = {
       instances: new Map(),
       currentArea: null,
       removable: true,
       overflows: true,
       defaultArea: null,
       shortcutId: null,
       tooltiptext: null,
       showInPrivateBrowsing: true,
+      _introducedInVersion: -1,
     };
 
     if (typeof aData.id != "string" || !/^[a-z0-9-_]{1,}$/i.test(aData.id)) {
       ERROR("Given an illegal id in normalizeWidget: " + aData.id);
       return null;
     }
 
     delete widget.implementation.currentArea;
@@ -2089,33 +2121,39 @@ let CustomizableUIInternal = {
 
     const kOptBoolProps = ["removable", "showInPrivateBrowsing", "overflows"];
     for (let prop of kOptBoolProps) {
       if (typeof aData[prop] == "boolean") {
         widget[prop] = aData[prop];
       }
     }
 
-    if (aData.defaultArea && gAreas.has(aData.defaultArea)) {
+    // When we normalize builtin widgets, areas have not yet been registered:
+    if (aData.defaultArea &&
+        (aSource == CustomizableUI.SOURCE_BUILTIN || gAreas.has(aData.defaultArea))) {
       widget.defaultArea = aData.defaultArea;
     } else if (!widget.removable) {
       ERROR("Widget '" + widget.id + "' is not removable but does not specify " +
             "a valid defaultArea. That's not possible; it must specify a " +
             "valid defaultArea as well.");
       return null;
     }
 
     if ("type" in aData && gSupportedWidgetTypes.has(aData.type)) {
       widget.type = aData.type;
     } else {
       widget.type = "button";
     }
 
     widget.disabled = aData.disabled === true;
 
+    if (aSource == CustomizableUI.SOURCE_BUILTIN) {
+      widget._introducedInVersion = aData.introducedInVersion || 0;
+    }
+
     this.wrapWidgetEventHandler("onBeforeCreated", widget);
     this.wrapWidgetEventHandler("onClick", widget);
     this.wrapWidgetEventHandler("onCreated", widget);
 
     if (widget.type == "button") {
       widget.onCommand = typeof aData.onCommand == "function" ?
                            aData.onCommand :
                            null;