Bug 970511 - [Australis] Allow users to undo the "restore default" action. r=Gijs
authorJared Wein <jwein@mozilla.com>
Wed, 12 Feb 2014 12:46:26 -0500
changeset 168853 0d95354a2ad5455db427f021dd9b1a7bc80ab25d
parent 168852 3761b11b90f059a05ca54d978d4cb00733e6628f
child 168854 dfde6f7df7f5965b2f5c3656c58ab53bd0942139
push id270
push userpvanderbeken@mozilla.com
push dateThu, 06 Mar 2014 09:24:21 +0000
reviewersGijs
bugs970511
milestone30.0a1
Bug 970511 - [Australis] Allow users to undo the "restore default" action. r=Gijs
browser/components/customizableui/content/customizeMode.inc.xul
browser/components/customizableui/src/CustomizableUI.jsm
browser/components/customizableui/src/CustomizeMode.jsm
browser/components/customizableui/test/browser.ini
browser/components/customizableui/test/browser_970511_undo_restore_default.js
browser/themes/shared/customizableui/customizeMode.inc.css
--- a/browser/components/customizableui/content/customizeMode.inc.xul
+++ b/browser/components/customizableui/content/customizeMode.inc.xul
@@ -25,16 +25,23 @@
 #NB: because oncommand fires after click, by the time we've fired, the checkbox binding
 #    will already have switched the button's state, so this is correct:
               oncommand="gCustomizeMode.toggleTitlebar(this.hasAttribute('checked'))"/>
 #endif
       <button id="customization-toolbar-visibility-button" label="&customizeMode.toolbars;" class="customizationmode-button" type="menu">
         <menupopup id="customization-toolbar-menu" onpopupshowing="onViewToolbarsPopupShowing(event)"/>
       </button>
       <spacer flex="1"/>
+      <label id="customization-undo-reset"
+             hidden="true"
+             onclick="gCustomizeMode.undoReset();"
+             onkeypress="gCustomizeMode.undoReset();"
+             class="text-link">
+        &undoCmd.label;
+      </label>
       <button id="customization-reset-button" oncommand="gCustomizeMode.reset();" label="&customizeMode.restoreDefaults;" class="customizationmode-button"/>
     </hbox>
   </vbox>
   <vbox id="customization-panel-container">
     <vbox id="customization-panelWrapper">
       <html:style html:type="text/html" scoped="scoped">
         @import url(chrome://global/skin/popup.css);
       </html:style>
--- a/browser/components/customizableui/src/CustomizableUI.jsm
+++ b/browser/components/customizableui/src/CustomizableUI.jsm
@@ -123,16 +123,18 @@ let gBuildAreas = new Map();
  */
 let gBuildWindows = new Map();
 
 let gNewElementCount = 0;
 let gGroupWrapperCache = new Map();
 let gSingleWrapperCache = new WeakMap();
 let gListeners = new Set();
 
+let gUIStateBeforeReset = null;
+
 let gModuleName = "[CustomizableUI]";
 #include logging.js
 
 let CustomizableUIInternal = {
   initialize: function() {
     LOG("Initializing");
 
     this.addListener(this);
@@ -688,16 +690,20 @@ let CustomizableUIInternal = {
       child.setAttribute("wrap", "true");
     }
 
     this.registerBuildArea(CustomizableUI.AREA_PANEL, aPanelContents);
   },
 
   onWidgetAdded: function(aWidgetId, aArea, aPosition) {
     this.insertNode(aWidgetId, aArea, aPosition, true);
+
+    if (!gResetting) {
+      gUIStateBeforeReset = null;
+    }
   },
 
   onWidgetRemoved: function(aWidgetId, aArea) {
     let areaNodes = gBuildAreas.get(aArea);
     if (!areaNodes) {
       return;
     }
 
@@ -744,20 +750,30 @@ let CustomizableUIInternal = {
         areaNode.setAttribute("currentset", gPlacements.get(aArea).join(','));
       }
 
       let windowCache = gSingleWrapperCache.get(window);
       if (windowCache) {
         windowCache.delete(aWidgetId);
       }
     }
+    if (!gResetting) {
+      gUIStateBeforeReset = null;
+    }
   },
 
   onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) {
     this.insertNode(aWidgetId, aArea, aNewPosition);
+    if (!gResetting) {
+      gUIStateBeforeReset = null;
+    }
+  },
+
+  onCustomizeEnd: function(aWindow) {
+    gUIStateBeforeReset = null;
   },
 
   registerBuildArea: function(aArea, aNode) {
     // We ensure that the window is registered to have its customization data
     // cleaned up when unloading.
     let window = aNode.ownerDocument.defaultView;
     if (window.closed) {
       return;
@@ -2044,46 +2060,76 @@ let CustomizableUIInternal = {
       }
     }
 
     return null;
   },
 
   reset: function() {
     gResetting = true;
+    this._resetUIState();
+
+    // Rebuild each registered area (across windows) to reflect the state that
+    // was reset above.
+    this._rebuildRegisteredAreas();
+
+    gResetting = false;
+  },
+
+  _resetUIState: function() {
+    try {
+      gUIStateBeforeReset = Services.prefs.getCharPref(kPrefCustomizationState);
+    } catch(e) { }
+
     Services.prefs.clearUserPref(kPrefCustomizationState);
     LOG("State reset");
 
     // Reset placements to make restoring default placements possible.
     gPlacements = new Map();
     gDirtyAreaCache = new Set();
     gSeenWidgets = new Set();
     // Clear the saved state to ensure that defaults will be used.
     gSavedState = null;
     // Restore the state for each area to its defaults
     for (let [areaId,] of gAreas) {
       this.restoreStateForArea(areaId);
     }
-
-    // Rebuild each registered area (across windows) to reflect the state that
-    // was reset above.
+  },
+
+  _rebuildRegisteredAreas: function() {
     for (let [areaId, areaNodes] of gBuildAreas) {
       let placements = gPlacements.get(areaId);
       for (let areaNode of areaNodes) {
         this.buildArea(areaId, placements, areaNode);
 
         let area = gAreas.get(areaId);
         if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) {
           let defaultCollapsed = area.get("defaultCollapsed");
           let win = areaNode.ownerDocument.defaultView;
           win.setToolbarVisibility(areaNode, !defaultCollapsed);
         }
       }
     }
-    gResetting = false;
+  },
+
+  /**
+   * Undoes a previous reset, restoring the state of the UI to the state prior to the reset.
+   */
+  undoReset: function() {
+    if (!gUIStateBeforeReset) {
+      return;
+    }
+    Services.prefs.setCharPref(kPrefCustomizationState, gUIStateBeforeReset);
+    this.loadSavedState();
+    for (let areaId of Object.keys(gSavedState.placements)) {
+      let placements = gSavedState.placements[areaId];
+      gPlacements.set(areaId, placements);
+    }
+    this._rebuildRegisteredAreas();
+    gUIStateBeforeReset = null;
   },
 
   /**
    * @param {String|Node} aWidget - widget ID or a widget node (preferred for performance).
    * @return {Boolean} whether the widget is removable
    */
   isWidgetRemovable: function(aWidget) {
     let widgetId;
@@ -2827,16 +2873,35 @@ this.CustomizableUI = {
    *
    * This is the nuclear option. You should never call this except if the user
    * explicitly requests it. Firefox does this when the user clicks the
    * "Restore Defaults" button in customize mode.
    */
   reset: function() {
     CustomizableUIInternal.reset();
   },
+
+  /**
+   * Undo the previous reset, can only be called immediately after a reset.
+   * @return a promise that will be resolved when the operation is complete.
+   */
+  undoReset: function() {
+    CustomizableUIInternal.undoReset();
+  },
+
+  /**
+   * Can the last Restore Defaults operation be undone.
+   *
+   * @return A boolean stating whether an undo of the
+   *         Restore Defaults can be performed.
+   */
+  get canUndoReset() {
+    return !!gUIStateBeforeReset;
+  },
+
   /**
    * Get the placement of a widget. This is by far the best way to obtain
    * information about what the state of your widget is. The internals of
    * this call are cheap (no DOM necessary) and you will know where the user
    * has put your widget.
    *
    * @param aWidgetId the ID of the widget whose placement you want to know
    * @return
--- a/browser/components/customizableui/src/CustomizeMode.jsm
+++ b/browser/components/customizableui/src/CustomizeMode.jsm
@@ -234,16 +234,17 @@ CustomizeMode.prototype = {
       this.visiblePalette.addEventListener("dragend", this, true);
 
       window.gNavToolbox.addEventListener("toolbarvisibilitychange", this);
 
       document.getElementById("PanelUI-help").setAttribute("disabled", true);
       document.getElementById("PanelUI-quit").setAttribute("disabled", true);
 
       this._updateResetButton();
+      this._updateUndoResetButton();
 
       this._skipSourceNodeCheck = Services.prefs.getPrefType(kSkipSourceNodePref) == Ci.nsIPrefBranch.PREF_BOOL &&
                                   Services.prefs.getBoolPref(kSkipSourceNodePref);
 
       let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true]):not([collapsed=true])");
       for (let toolbar of customizableToolbars)
         toolbar.setAttribute("customizing", true);
 
@@ -849,22 +850,45 @@ CustomizeMode.prototype = {
       CustomizableUI.reset();
 
       yield this._wrapToolbarItems();
       yield this.populatePalette();
 
       this.persistCurrentSets(true);
 
       this._updateResetButton();
+      this._updateUndoResetButton();
       this._updateEmptyPaletteNotice();
       this._showPanelCustomizationPlaceholders();
       this.resetting = false;
     }.bind(this)).then(null, ERROR);
   },
 
+  undoReset: function() {
+    this.resetting = true;
+
+    return Task.spawn(function() {
+      this._removePanelCustomizationPlaceholders();
+      yield this.depopulatePalette();
+      yield this._unwrapToolbarItems();
+
+      CustomizableUI.undoReset();
+
+      yield this._wrapToolbarItems();
+      yield this.populatePalette();
+
+      this.persistCurrentSets(true);
+
+      this._updateResetButton();
+      this._updateUndoResetButton();
+      this._updateEmptyPaletteNotice();
+      this.resetting = false;
+    }.bind(this)).then(null, ERROR);
+  },
+
   _onToolbarVisibilityChange: function(aEvent) {
     let toolbar = aEvent.target;
     if (aEvent.detail.visible && toolbar.getAttribute("customizable") == "true") {
       toolbar.setAttribute("customizing", "true");
     } else {
       toolbar.removeAttribute("customizing");
     }
     this._onUIChange();
@@ -953,31 +977,37 @@ CustomizeMode.prototype = {
       }
     }
   },
 
   _onUIChange: function() {
     this._changed = true;
     if (!this.resetting) {
       this._updateResetButton();
+      this._updateUndoResetButton();
       this._updateEmptyPaletteNotice();
     }
     this.dispatchToolboxEvent("customizationchange");
   },
 
   _updateEmptyPaletteNotice: function() {
     let paletteItems = this.visiblePalette.getElementsByTagName("toolbarpaletteitem");
     this.paletteEmptyNotice.hidden = !!paletteItems.length;
   },
 
   _updateResetButton: function() {
     let btn = this.document.getElementById("customization-reset-button");
     btn.disabled = CustomizableUI.inDefaultState;
   },
 
+  _updateUndoResetButton: function() {
+    let undoReset =  this.document.getElementById("customization-undo-reset");
+    undoReset.hidden = !CustomizableUI.canUndoReset;
+  },
+
   handleEvent: function(aEvent) {
     switch(aEvent.type) {
       case "toolbarvisibilitychange":
         this._onToolbarVisibilityChange(aEvent);
         break;
       case "dragstart":
         this._onDragStart(aEvent);
         break;
--- a/browser/components/customizableui/test/browser.ini
+++ b/browser/components/customizableui/test/browser.ini
@@ -60,9 +60,10 @@ skip-if = os == "linux"
 [browser_944887_destroyWidget_should_destroy_in_palette.js]
 [browser_945739_showInPrivateBrowsing_customize_mode.js]
 [browser_947987_removable_default.js]
 [browser_948985_non_removable_defaultArea.js]
 [browser_952963_areaType_getter_no_area.js]
 [browser_956602_remove_special_widget.js]
 [browser_969427_recreate_destroyed_widget_after_reset.js]
 [browser_969661_character_encoding_navbar_disabled.js]
+[browser_970511_undo_restore_default.js]
 [browser_panel_toggle.js]
new file mode 100644
--- /dev/null
+++ b/browser/components/customizableui/test/browser_970511_undo_restore_default.js
@@ -0,0 +1,65 @@
+/* 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";
+
+// Restoring default should show an "undo" option which undoes the restoring operation.
+add_task(function() {
+  let homeButtonId = "home-button";
+  CustomizableUI.removeWidgetFromArea(homeButtonId);
+  yield startCustomizing();
+  ok(!CustomizableUI.inDefaultState, "Not in default state to begin with");
+  is(CustomizableUI.getPlacementOfWidget(homeButtonId), null, "Home button is in palette");
+  let undoReset = document.getElementById("customization-undo-reset");
+  is(undoReset.hidden, true, "The undo button is hidden before reset");
+
+  yield gCustomizeMode.reset();
+
+  ok(CustomizableUI.inDefaultState, "In default state after reset");
+  is(undoReset.hidden, false, "The undo button is visible after reset");
+
+  undoReset.click();
+  yield waitForCondition(function() !gCustomizeMode.resetting);
+  ok(!CustomizableUI.inDefaultState, "Not in default state after reset-undo");
+  is(undoReset.hidden, true, "The undo button is hidden after clicking on the undo button");
+  is(CustomizableUI.getPlacementOfWidget(homeButtonId), null, "Home button is in palette");
+
+  yield gCustomizeMode.reset();
+});
+
+// Performing an action after a reset will hide the reset button.
+add_task(function() {
+  let homeButtonId = "home-button";
+  CustomizableUI.removeWidgetFromArea(homeButtonId);
+  ok(!CustomizableUI.inDefaultState, "Not in default state to begin with");
+  is(CustomizableUI.getPlacementOfWidget(homeButtonId), null, "Home button is in palette");
+  let undoReset = document.getElementById("customization-undo-reset");
+  is(undoReset.hidden, true, "The undo button is hidden before reset");
+
+  yield gCustomizeMode.reset();
+
+  ok(CustomizableUI.inDefaultState, "In default state after reset");
+  is(undoReset.hidden, false, "The undo button is visible after reset");
+
+  CustomizableUI.addWidgetToArea(homeButtonId, CustomizableUI.AREA_PANEL);
+  is(undoReset.hidden, true, "The undo button is hidden after another change");
+});
+
+// "Restore defaults", exiting customize, and re-entering shouldn't show the Undo button
+add_task(function() {
+  let undoReset = document.getElementById("customization-undo-reset");
+  is(undoReset.hidden, true, "The undo button is hidden before a reset");
+  ok(!CustomizableUI.inDefaultState, "The browser should not be in default state");
+  yield gCustomizeMode.reset();
+
+  is(undoReset.hidden, false, "The undo button is hidden after a reset");
+  yield endCustomizing();
+  yield startCustomizing();
+  is(undoReset.hidden, true, "The undo reset button should be hidden after entering customization mode");
+});
+
+add_task(function asyncCleanup() {
+  yield gCustomizeMode.reset();
+  yield endCustomizing();
+});
--- a/browser/themes/shared/customizableui/customizeMode.inc.css
+++ b/browser/themes/shared/customizableui/customizeMode.inc.css
@@ -99,16 +99,25 @@
   -moz-image-region: rect(0, 48px, 24px, 24px);
   background-color: rgb(218, 218, 218);
   border-color: rgb(168, 168, 168);
   text-shadow: 0 1px rgb(236, 236, 236);
   box-shadow: 0 1px rgba(255, 255, 255, 0.5),
               inset 0 1px rgb(196, 196, 196);
 }
 
+#customization-undo-reset {
+  padding-left: 12px;
+  padding-right: 12px;
+%ifdef XP_MACOSX
+  padding-top: 6px;
+%else
+  padding-top: 7px;
+%endif
+}
 
 #main-window[customize-entered] #customization-panel-container {
   background-image: url("chrome://browser/skin/customizableui/customizeMode-separatorHorizontal.png"),
                     url("chrome://browser/skin/customizableui/customizeMode-separatorVertical.png"),
                     url("chrome://browser/skin/customizableui/customizeMode-gridTexture.png"),
                     url("chrome://browser/skin/customizableui/background-noise-toolbar.png"),
                     linear-gradient(to bottom, #3e86ce, #3878ba);
   background-position: center top, left center, left top, left top, left top;