browser/components/customizableui/src/CustomizableUI.jsm

Wed, 31 Dec 2014 13:27:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 13:27:57 +0100
branch
TOR_BUG_3246
changeset 6
8bccb770b82d
permissions
-rw-r--r--

Ignore runtime configuration files generated during quality assurance.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 this.EXPORTED_SYMBOLS = ["CustomizableUI"];
     9 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
    11 Cu.import("resource://gre/modules/Services.jsm");
    12 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
    13 XPCOMUtils.defineLazyModuleGetter(this, "PanelWideWidgetTracker",
    14   "resource:///modules/PanelWideWidgetTracker.jsm");
    15 XPCOMUtils.defineLazyModuleGetter(this, "CustomizableWidgets",
    16   "resource:///modules/CustomizableWidgets.jsm");
    17 XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
    18   "resource://gre/modules/DeferredTask.jsm");
    19 XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
    20   "resource://gre/modules/PrivateBrowsingUtils.jsm");
    21 XPCOMUtils.defineLazyModuleGetter(this, "Promise",
    22   "resource://gre/modules/Promise.jsm");
    23 XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() {
    24   const kUrl = "chrome://browser/locale/customizableui/customizableWidgets.properties";
    25   return Services.strings.createBundle(kUrl);
    26 });
    27 XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
    28   "resource://gre/modules/ShortcutUtils.jsm");
    29 XPCOMUtils.defineLazyServiceGetter(this, "gELS",
    30   "@mozilla.org/eventlistenerservice;1", "nsIEventListenerService");
    32 const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
    34 const kSpecialWidgetPfx = "customizableui-special-";
    36 const kPrefCustomizationState        = "browser.uiCustomization.state";
    37 const kPrefCustomizationAutoAdd      = "browser.uiCustomization.autoAdd";
    38 const kPrefCustomizationDebug        = "browser.uiCustomization.debug";
    39 const kPrefDrawInTitlebar            = "browser.tabs.drawInTitlebar";
    41 /**
    42  * The keys are the handlers that are fired when the event type (the value)
    43  * is fired on the subview. A widget that provides a subview has the option
    44  * of providing onViewShowing and onViewHiding event handlers.
    45  */
    46 const kSubviewEvents = [
    47   "ViewShowing",
    48   "ViewHiding"
    49 ];
    51 /**
    52  * gPalette is a map of every widget that CustomizableUI.jsm knows about, keyed
    53  * on their IDs.
    54  */
    55 let gPalette = new Map();
    57 /**
    58  * gAreas maps area IDs to Sets of properties about those areas. An area is a
    59  * place where a widget can be put.
    60  */
    61 let gAreas = new Map();
    63 /**
    64  * gPlacements maps area IDs to Arrays of widget IDs, indicating that the widgets
    65  * are placed within that area (either directly in the area node, or in the
    66  * customizationTarget of the node).
    67  */
    68 let gPlacements = new Map();
    70 /**
    71  * gFuturePlacements represent placements that will happen for areas that have
    72  * not yet loaded (due to lazy-loading). This can occur when add-ons register
    73  * widgets.
    74  */
    75 let gFuturePlacements = new Map();
    77 //XXXunf Temporary. Need a nice way to abstract functions to build widgets
    78 //       of these types.
    79 let gSupportedWidgetTypes = new Set(["button", "view", "custom"]);
    81 /**
    82  * gPanelsForWindow is a list of known panels in a window which we may need to close
    83  * should command events fire which target them.
    84  */
    85 let gPanelsForWindow = new WeakMap();
    87 /**
    88  * gSeenWidgets remembers which widgets the user has seen for the first time
    89  * before. This way, if a new widget is created, and the user has not seen it
    90  * before, it can be put in its default location. Otherwise, it remains in the
    91  * palette.
    92  */
    93 let gSeenWidgets = new Set();
    95 /**
    96  * gDirtyAreaCache is a set of area IDs for areas where items have been added,
    97  * moved or removed at least once. This set is persisted, and is used to
    98  * optimize building of toolbars in the default case where no toolbars should
    99  * be "dirty".
   100  */
   101 let gDirtyAreaCache = new Set();
   103 /**
   104  * gPendingBuildAreas is a map from area IDs to map from build nodes to their
   105  * existing children at the time of node registration, that are waiting
   106  * for the area to be registered
   107  */
   108 let gPendingBuildAreas = new Map();
   110 let gSavedState = null;
   111 let gRestoring = false;
   112 let gDirty = false;
   113 let gInBatchStack = 0;
   114 let gResetting = false;
   115 let gUndoResetting = false;
   117 /**
   118  * gBuildAreas maps area IDs to actual area nodes within browser windows.
   119  */
   120 let gBuildAreas = new Map();
   122 /**
   123  * gBuildWindows is a map of windows that have registered build areas, mapped
   124  * to a Set of known toolboxes in that window.
   125  */
   126 let gBuildWindows = new Map();
   128 let gNewElementCount = 0;
   129 let gGroupWrapperCache = new Map();
   130 let gSingleWrapperCache = new WeakMap();
   131 let gListeners = new Set();
   133 let gUIStateBeforeReset = {
   134   uiCustomizationState: null,
   135   drawInTitlebar: null,
   136 };
   138 let gModuleName = "[CustomizableUI]";
   139 #include logging.js
   141 let CustomizableUIInternal = {
   142   initialize: function() {
   143     LOG("Initializing");
   145     this.addListener(this);
   146     this._defineBuiltInWidgets();
   147     this.loadSavedState();
   149     let panelPlacements = [
   150       "edit-controls",
   151       "zoom-controls",
   152       "new-window-button",
   153       "privatebrowsing-button",
   154       "save-page-button",
   155       "print-button",
   156       "history-panelmenu",
   157       "fullscreen-button",
   158       "find-button",
   159       "preferences-button",
   160       "add-ons-button",
   161       "developer-button",
   162     ];
   164     if (gPalette.has("switch-to-metro-button")) {
   165       panelPlacements.push("switch-to-metro-button");
   166     }
   168 #ifdef NIGHTLY_BUILD
   169     if (gPalette.has("e10s-button")) {
   170       let newWindowIndex = panelPlacements.indexOf("new-window-button");
   171       if (newWindowIndex > -1) {
   172         panelPlacements.splice(newWindowIndex + 1, 0, "e10s-button");
   173       }
   174     }
   175 #endif
   177     let showCharacterEncoding = Services.prefs.getComplexValue(
   178       "browser.menu.showCharacterEncoding",
   179       Ci.nsIPrefLocalizedString
   180     ).data;
   181     if (showCharacterEncoding == "true") {
   182       panelPlacements.push("characterencoding-button");
   183     }
   185     this.registerArea(CustomizableUI.AREA_PANEL, {
   186       anchor: "PanelUI-menu-button",
   187       type: CustomizableUI.TYPE_MENU_PANEL,
   188       defaultPlacements: panelPlacements
   189     }, true);
   190     PanelWideWidgetTracker.init();
   192     this.registerArea(CustomizableUI.AREA_NAVBAR, {
   193       legacy: true,
   194       type: CustomizableUI.TYPE_TOOLBAR,
   195       overflowable: true,
   196       defaultPlacements: [
   197         "urlbar-container",
   198         "search-container",
   199         "webrtc-status-button",
   200         "bookmarks-menu-button",
   201         "downloads-button",
   202         "home-button",
   203         "social-share-button",
   204       ],
   205       defaultCollapsed: false,
   206     }, true);
   207 #ifndef XP_MACOSX
   208     this.registerArea(CustomizableUI.AREA_MENUBAR, {
   209       legacy: true,
   210       type: CustomizableUI.TYPE_TOOLBAR,
   211       defaultPlacements: [
   212         "menubar-items",
   213       ],
   214       get defaultCollapsed() {
   215 #ifdef MENUBAR_CAN_AUTOHIDE
   216 #if defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_QT)
   217         return true;
   218 #else
   219         // This is duplicated logic from /browser/base/jar.mn
   220         // for win6BrowserOverlay.xul.
   221         return Services.appinfo.OS == "WINNT" &&
   222                Services.sysinfo.getProperty("version") != "5.1";
   223 #endif
   224 #endif
   225         return false;
   226       }
   227     }, true);
   228 #endif
   229     this.registerArea(CustomizableUI.AREA_TABSTRIP, {
   230       legacy: true,
   231       type: CustomizableUI.TYPE_TOOLBAR,
   232       defaultPlacements: [
   233         "tabbrowser-tabs",
   234         "new-tab-button",
   235         "alltabs-button",
   236       ],
   237       defaultCollapsed: null,
   238     }, true);
   239     this.registerArea(CustomizableUI.AREA_BOOKMARKS, {
   240       legacy: true,
   241       type: CustomizableUI.TYPE_TOOLBAR,
   242       defaultPlacements: [
   243         "personal-bookmarks",
   244       ],
   245       defaultCollapsed: true,
   246     }, true);
   248     this.registerArea(CustomizableUI.AREA_ADDONBAR, {
   249       type: CustomizableUI.TYPE_TOOLBAR,
   250       legacy: true,
   251       defaultPlacements: ["addonbar-closebutton", "status-bar"],
   252       defaultCollapsed: false,
   253     }, true);
   254   },
   256   get _builtinToolbars() {
   257     return new Set([
   258       CustomizableUI.AREA_NAVBAR,
   259       CustomizableUI.AREA_BOOKMARKS,
   260       CustomizableUI.AREA_TABSTRIP,
   261       CustomizableUI.AREA_ADDONBAR,
   262 #ifndef XP_MACOSX
   263       CustomizableUI.AREA_MENUBAR,
   264 #endif
   265     ]);
   266   },
   268   _defineBuiltInWidgets: function() {
   269     //XXXunf Need to figure out how to auto-add new builtin widgets in new
   270     //       app versions to already customized areas.
   271     for (let widgetDefinition of CustomizableWidgets) {
   272       this.createBuiltinWidget(widgetDefinition);
   273     }
   274   },
   276   wrapWidget: function(aWidgetId) {
   277     if (gGroupWrapperCache.has(aWidgetId)) {
   278       return gGroupWrapperCache.get(aWidgetId);
   279     }
   281     let provider = this.getWidgetProvider(aWidgetId);
   282     if (!provider) {
   283       return null;
   284     }
   286     if (provider == CustomizableUI.PROVIDER_API) {
   287       let widget = gPalette.get(aWidgetId);
   288       if (!widget.wrapper) {
   289         widget.wrapper = new WidgetGroupWrapper(widget);
   290         gGroupWrapperCache.set(aWidgetId, widget.wrapper);
   291       }
   292       return widget.wrapper;
   293     }
   295     // PROVIDER_SPECIAL gets treated the same as PROVIDER_XUL.
   296     let wrapper = new XULWidgetGroupWrapper(aWidgetId);
   297     gGroupWrapperCache.set(aWidgetId, wrapper);
   298     return wrapper;
   299   },
   301   registerArea: function(aName, aProperties, aInternalCaller) {
   302     if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
   303       throw new Error("Invalid area name");
   304     }
   306     let areaIsKnown = gAreas.has(aName);
   307     let props = areaIsKnown ? gAreas.get(aName) : new Map();
   308     const kImmutableProperties = new Set(["type", "legacy", "overflowable"]);
   309     for (let key in aProperties) {
   310       if (areaIsKnown && kImmutableProperties.has(key) &&
   311           props.get(key) != aProperties[key]) {
   312         throw new Error("An area cannot change the property for '" + key + "'");
   313       }
   314       //XXXgijs for special items, we need to make sure they have an appropriate ID
   315       // so we aren't perpetually in a non-default state:
   316       if (key == "defaultPlacements" && Array.isArray(aProperties[key])) {
   317         props.set(key, aProperties[key].map(x => this.isSpecialWidget(x) ? this.ensureSpecialWidgetId(x) : x ));
   318       } else {
   319         props.set(key, aProperties[key]);
   320       }
   321     }
   322     // Default to a toolbar:
   323     if (!props.has("type")) {
   324       props.set("type", CustomizableUI.TYPE_TOOLBAR);
   325     }
   326     if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
   327       // Check aProperties instead of props because this check is only interested
   328       // in the passed arguments, not the state of a potentially pre-existing area.
   329       if (!aInternalCaller && aProperties["defaultCollapsed"]) {
   330         throw new Error("defaultCollapsed is only allowed for default toolbars.")
   331       }
   332       if (!props.has("defaultCollapsed")) {
   333         props.set("defaultCollapsed", true);
   334       }
   335     } else if (props.has("defaultCollapsed")) {
   336       throw new Error("defaultCollapsed only applies for TYPE_TOOLBAR areas.");
   337     }
   338     // Sanity check type:
   339     let allTypes = [CustomizableUI.TYPE_TOOLBAR, CustomizableUI.TYPE_MENU_PANEL];
   340     if (allTypes.indexOf(props.get("type")) == -1) {
   341       throw new Error("Invalid area type " + props.get("type"));
   342     }
   344     // And to no placements:
   345     if (!props.has("defaultPlacements")) {
   346       props.set("defaultPlacements", []);
   347     }
   348     // Sanity check default placements array:
   349     if (!Array.isArray(props.get("defaultPlacements"))) {
   350       throw new Error("Should provide an array of default placements");
   351     }
   353     if (!areaIsKnown) {
   354       gAreas.set(aName, props);
   356       if (props.get("legacy") && !gPlacements.has(aName)) {
   357         // Guarantee this area exists in gFuturePlacements, to avoid checking it in
   358         // various places elsewhere.
   359         gFuturePlacements.set(aName, new Set());
   360       } else {
   361         this.restoreStateForArea(aName);
   362       }
   364       // If we have pending build area nodes, register all of them
   365       if (gPendingBuildAreas.has(aName)) {
   366         let pendingNodes = gPendingBuildAreas.get(aName);
   367         for (let [pendingNode, existingChildren] of pendingNodes) {
   368           this.registerToolbarNode(pendingNode, existingChildren);
   369         }
   370         gPendingBuildAreas.delete(aName);
   371       }
   372     }
   373   },
   375   unregisterArea: function(aName, aDestroyPlacements) {
   376     if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
   377       throw new Error("Invalid area name");
   378     }
   379     if (!gAreas.has(aName) && !gPlacements.has(aName)) {
   380       throw new Error("Area not registered");
   381     }
   383     // Move all the widgets out
   384     this.beginBatchUpdate();
   385     try {
   386       let placements = gPlacements.get(aName);
   387       if (placements) {
   388         // Need to clone this array so removeWidgetFromArea doesn't modify it
   389         placements = [...placements];
   390         placements.forEach(this.removeWidgetFromArea, this);
   391       }
   393       // Delete all remaining traces.
   394       gAreas.delete(aName);
   395       // Only destroy placements when necessary:
   396       if (aDestroyPlacements) {
   397         gPlacements.delete(aName);
   398       } else {
   399         // Otherwise we need to re-set them, as removeFromArea will have emptied
   400         // them out:
   401         gPlacements.set(aName, placements);
   402       }
   403       gFuturePlacements.delete(aName);
   404       let existingAreaNodes = gBuildAreas.get(aName);
   405       if (existingAreaNodes) {
   406         for (let areaNode of existingAreaNodes) {
   407           this.notifyListeners("onAreaNodeUnregistered", aName, areaNode.customizationTarget,
   408                                CustomizableUI.REASON_AREA_UNREGISTERED);
   409         }
   410       }
   411       gBuildAreas.delete(aName);
   412     } finally {
   413       this.endBatchUpdate(true);
   414     }
   415   },
   417   registerToolbarNode: function(aToolbar, aExistingChildren) {
   418     let area = aToolbar.id;
   419     if (gBuildAreas.has(area) && gBuildAreas.get(area).has(aToolbar)) {
   420       return;
   421     }
   422     let document = aToolbar.ownerDocument;
   423     let areaProperties = gAreas.get(area);
   425     // If this area is not registered, try to do it automatically:
   426     if (!areaProperties) {
   427       // If there's no defaultset attribute and this isn't a legacy extra toolbar,
   428       // we assume that we should wait for registerArea to be called:
   429       if (!aToolbar.hasAttribute("defaultset") &&
   430           !aToolbar.hasAttribute("customindex")) {
   431         if (!gPendingBuildAreas.has(area)) {
   432           gPendingBuildAreas.set(area, new Map());
   433         }
   434         let pendingNodes = gPendingBuildAreas.get(area);
   435         pendingNodes.set(aToolbar, aExistingChildren);
   436         return;
   437       }
   438       let props = {type: CustomizableUI.TYPE_TOOLBAR, legacy: true};
   439       let defaultsetAttribute = aToolbar.getAttribute("defaultset") || "";
   440       props.defaultPlacements = defaultsetAttribute.split(',').filter(s => s);
   441       this.registerArea(area, props);
   442       areaProperties = gAreas.get(area);
   443     }
   445     this.beginBatchUpdate();
   446     try {
   447       let placements = gPlacements.get(area);
   448       if (!placements && areaProperties.has("legacy")) {
   449         let legacyState = aToolbar.getAttribute("currentset");
   450         if (legacyState) {
   451           legacyState = legacyState.split(",").filter(s => s);
   452         }
   454         // Manually restore the state here, so the legacy state can be converted. 
   455         this.restoreStateForArea(area, legacyState);
   456         placements = gPlacements.get(area);
   457       }
   459       // Check that the current children and the current placements match. If
   460       // not, mark it as dirty:
   461       if (aExistingChildren.length != placements.length ||
   462           aExistingChildren.every((id, i) => id == placements[i])) {
   463         gDirtyAreaCache.add(area);
   464       }
   466       if (areaProperties.has("overflowable")) {
   467         aToolbar.overflowable = new OverflowableToolbar(aToolbar);
   468       }
   470       this.registerBuildArea(area, aToolbar);
   472       // We only build the toolbar if it's been marked as "dirty". Dirty means
   473       // one of the following things:
   474       // 1) Items have been added, moved or removed from this toolbar before.
   475       // 2) The number of children of the toolbar does not match the length of
   476       //    the placements array for that area.
   477       //
   478       // This notion of being "dirty" is stored in a cache which is persisted
   479       // in the saved state.
   480       if (gDirtyAreaCache.has(area)) {
   481         this.buildArea(area, placements, aToolbar);
   482       }
   483       this.notifyListeners("onAreaNodeRegistered", area, aToolbar.customizationTarget);
   484       aToolbar.setAttribute("currentset", placements.join(","));
   485     } finally {
   486       this.endBatchUpdate();
   487     }
   488   },
   490   buildArea: function(aArea, aPlacements, aAreaNode) {
   491     let document = aAreaNode.ownerDocument;
   492     let window = document.defaultView;
   493     let inPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(window);
   494     let container = aAreaNode.customizationTarget;
   495     let areaIsPanel = gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL;
   497     if (!container) {
   498       throw new Error("Expected area " + aArea
   499                       + " to have a customizationTarget attribute.");
   500     }
   502     // Restore nav-bar visibility since it may have been hidden
   503     // through a migration path (bug 938980) or an add-on.
   504     if (aArea == CustomizableUI.AREA_NAVBAR) {
   505       aAreaNode.collapsed = false;
   506     }
   508     this.beginBatchUpdate();
   510     try {
   511       let currentNode = container.firstChild;
   512       let placementsToRemove = new Set();
   513       for (let id of aPlacements) {
   514         while (currentNode && currentNode.getAttribute("skipintoolbarset") == "true") {
   515           currentNode = currentNode.nextSibling;
   516         }
   518         if (currentNode && currentNode.id == id) {
   519           currentNode = currentNode.nextSibling;
   520           continue;
   521         }
   523         if (this.isSpecialWidget(id) && areaIsPanel) {
   524           placementsToRemove.add(id);
   525           continue;
   526         }
   528         let [provider, node] = this.getWidgetNode(id, window);
   529         if (!node) {
   530           LOG("Unknown widget: " + id);
   531           continue;
   532         }
   534         // If the placements have items in them which are (now) no longer removable,
   535         // we shouldn't be moving them:
   536         if (provider == CustomizableUI.PROVIDER_API) {
   537           let widgetInfo = gPalette.get(id);
   538           if (!widgetInfo.removable && aArea != widgetInfo.defaultArea) {
   539             placementsToRemove.add(id);
   540             continue;
   541           }
   542         } else if (provider == CustomizableUI.PROVIDER_XUL &&
   543                    node.parentNode != container && !this.isWidgetRemovable(node)) {
   544           placementsToRemove.add(id);
   545           continue;
   546         } // Special widgets are always removable, so no need to check them
   548         if (inPrivateWindow && provider == CustomizableUI.PROVIDER_API) {
   549           let widget = gPalette.get(id);
   550           if (!widget.showInPrivateBrowsing && inPrivateWindow) {
   551             continue;
   552           }
   553         }
   555         this.ensureButtonContextMenu(node, aAreaNode);
   556         if (node.localName == "toolbarbutton") {
   557           if (areaIsPanel) {
   558             node.setAttribute("wrap", "true");
   559           } else {
   560             node.removeAttribute("wrap");
   561           }
   562         }
   564         this.insertWidgetBefore(node, currentNode, container, aArea);
   565         if (gResetting) {
   566           this.notifyListeners("onWidgetReset", node, container);
   567         } else if (gUndoResetting) {
   568           this.notifyListeners("onWidgetUndoMove", node, container);
   569         }
   570       }
   572       if (currentNode) {
   573         let palette = aAreaNode.toolbox ? aAreaNode.toolbox.palette : null;
   574         let limit = currentNode.previousSibling;
   575         let node = container.lastChild;
   576         while (node && node != limit) {
   577           let previousSibling = node.previousSibling;
   578           // Nodes opt-in to removability. If they're removable, and we haven't
   579           // seen them in the placements array, then we toss them into the palette
   580           // if one exists. If no palette exists, we just remove the node. If the
   581           // node is not removable, we leave it where it is. However, we can only
   582           // safely touch elements that have an ID - both because we depend on
   583           // IDs, and because such elements are not intended to be widgets
   584           // (eg, titlebar-placeholder elements).
   585           if (node.id && node.getAttribute("skipintoolbarset") != "true") {
   586             if (this.isWidgetRemovable(node)) {
   587               if (palette && !this.isSpecialWidget(node.id)) {
   588                 palette.appendChild(node);
   589                 this.removeLocationAttributes(node);
   590               } else {
   591                 container.removeChild(node);
   592               }
   593             } else {
   594               this.setLocationAttributes(currentNode, aArea);
   595               node.setAttribute("removable", false);
   596               LOG("Adding non-removable widget to placements of " + aArea + ": " +
   597                   node.id);
   598               gPlacements.get(aArea).push(node.id);
   599               gDirty = true;
   600             }
   601           }
   602           node = previousSibling;
   603         }
   604       }
   606       // If there are placements in here which aren't removable from their original area,
   607       // we remove them from this area's placement array. They will (have) be(en) added
   608       // to their original area's placements array in the block above this one.
   609       if (placementsToRemove.size) {
   610         let placementAry = gPlacements.get(aArea);
   611         for (let id of placementsToRemove) {
   612           let index = placementAry.indexOf(id);
   613           placementAry.splice(index, 1);
   614         }
   615       }
   617       if (gResetting) {
   618         this.notifyListeners("onAreaReset", aArea, container);
   619       }
   620     } finally {
   621       this.endBatchUpdate();
   622     }
   623   },
   625   addPanelCloseListeners: function(aPanel) {
   626     gELS.addSystemEventListener(aPanel, "click", this, false);
   627     gELS.addSystemEventListener(aPanel, "keypress", this, false);
   628     let win = aPanel.ownerDocument.defaultView;
   629     if (!gPanelsForWindow.has(win)) {
   630       gPanelsForWindow.set(win, new Set());
   631     }
   632     gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel));
   633   },
   635   removePanelCloseListeners: function(aPanel) {
   636     gELS.removeSystemEventListener(aPanel, "click", this, false);
   637     gELS.removeSystemEventListener(aPanel, "keypress", this, false);
   638     let win = aPanel.ownerDocument.defaultView;
   639     let panels = gPanelsForWindow.get(win);
   640     if (panels) {
   641       panels.delete(this._getPanelForNode(aPanel));
   642     }
   643   },
   645   ensureButtonContextMenu: function(aNode, aAreaNode) {
   646     const kPanelItemContextMenu = "customizationPanelItemContextMenu";
   648     let currentContextMenu = aNode.getAttribute("context") ||
   649                              aNode.getAttribute("contextmenu");
   650     let place = CustomizableUI.getPlaceForItem(aAreaNode);
   651     let contextMenuForPlace = place == "panel" ?
   652                                 kPanelItemContextMenu :
   653                                 null;
   654     if (contextMenuForPlace && !currentContextMenu) {
   655       aNode.setAttribute("context", contextMenuForPlace);
   656     } else if (currentContextMenu == kPanelItemContextMenu &&
   657                contextMenuForPlace != kPanelItemContextMenu) {
   658       aNode.removeAttribute("context");
   659       aNode.removeAttribute("contextmenu");
   660     }
   661   },
   663   getWidgetProvider: function(aWidgetId) {
   664     if (this.isSpecialWidget(aWidgetId)) {
   665       return CustomizableUI.PROVIDER_SPECIAL;
   666     }
   667     if (gPalette.has(aWidgetId)) {
   668       return CustomizableUI.PROVIDER_API;
   669     }
   670     // If this was an API widget that was destroyed, return null:
   671     if (gSeenWidgets.has(aWidgetId)) {
   672       return null;
   673     }
   675     // We fall back to the XUL provider, but we don't know for sure (at this
   676     // point) whether it exists there either. So the API is technically lying.
   677     // Ideally, it would be able to return an error value (or throw an
   678     // exception) if it really didn't exist. Our code calling this function
   679     // handles that fine, but this is a public API.
   680     return CustomizableUI.PROVIDER_XUL;
   681   },
   683   getWidgetNode: function(aWidgetId, aWindow) {
   684     let document = aWindow.document;
   686     if (this.isSpecialWidget(aWidgetId)) {
   687       let widgetNode = document.getElementById(aWidgetId) ||
   688                        this.createSpecialWidget(aWidgetId, document);
   689       return [ CustomizableUI.PROVIDER_SPECIAL, widgetNode];
   690     }
   692     let widget = gPalette.get(aWidgetId);
   693     if (widget) {
   694       // If we have an instance of this widget already, just use that.
   695       if (widget.instances.has(document)) {
   696         LOG("An instance of widget " + aWidgetId + " already exists in this "
   697             + "document. Reusing.");
   698         return [ CustomizableUI.PROVIDER_API,
   699                  widget.instances.get(document) ];
   700       }
   702       return [ CustomizableUI.PROVIDER_API,
   703                this.buildWidget(document, widget) ];
   704     }
   706     LOG("Searching for " + aWidgetId + " in toolbox.");
   707     let node = this.findWidgetInWindow(aWidgetId, aWindow);
   708     if (node) {
   709       return [ CustomizableUI.PROVIDER_XUL, node ];
   710     }
   712     LOG("No node for " + aWidgetId + " found.");
   713     return [null, null];
   714   },
   716   registerMenuPanel: function(aPanelContents) {
   717     if (gBuildAreas.has(CustomizableUI.AREA_PANEL) &&
   718         gBuildAreas.get(CustomizableUI.AREA_PANEL).has(aPanelContents)) {
   719       return;
   720     }
   722     let document = aPanelContents.ownerDocument;
   724     aPanelContents.toolbox = document.getElementById("navigator-toolbox");
   725     aPanelContents.customizationTarget = aPanelContents;
   727     this.addPanelCloseListeners(this._getPanelForNode(aPanelContents));
   729     let placements = gPlacements.get(CustomizableUI.AREA_PANEL);
   730     this.buildArea(CustomizableUI.AREA_PANEL, placements, aPanelContents);
   731     this.notifyListeners("onAreaNodeRegistered", CustomizableUI.AREA_PANEL, aPanelContents);
   733     for (let child of aPanelContents.children) {
   734       if (child.localName != "toolbarbutton") {
   735         if (child.localName == "toolbaritem") {
   736           this.ensureButtonContextMenu(child, aPanelContents);
   737         }
   738         continue;
   739       }
   740       this.ensureButtonContextMenu(child, aPanelContents);
   741       child.setAttribute("wrap", "true");
   742     }
   744     this.registerBuildArea(CustomizableUI.AREA_PANEL, aPanelContents);
   745   },
   747   onWidgetAdded: function(aWidgetId, aArea, aPosition) {
   748     this.insertNode(aWidgetId, aArea, aPosition, true);
   750     if (!gResetting) {
   751       this._clearPreviousUIState();
   752     }
   753   },
   755   onWidgetRemoved: function(aWidgetId, aArea) {
   756     let areaNodes = gBuildAreas.get(aArea);
   757     if (!areaNodes) {
   758       return;
   759     }
   761     let area = gAreas.get(aArea);
   762     let isToolbar = area.get("type") == CustomizableUI.TYPE_TOOLBAR;
   763     let isOverflowable = isToolbar && area.get("overflowable");
   764     let showInPrivateBrowsing = gPalette.has(aWidgetId)
   765                               ? gPalette.get(aWidgetId).showInPrivateBrowsing
   766                               : true;
   768     for (let areaNode of areaNodes) {
   769       let window = areaNode.ownerDocument.defaultView;
   770       if (!showInPrivateBrowsing &&
   771           PrivateBrowsingUtils.isWindowPrivate(window)) {
   772         continue;
   773       }
   775       let widgetNode = window.document.getElementById(aWidgetId);
   776       if (!widgetNode) {
   777         INFO("Widget not found, unable to remove");
   778         continue;
   779       }
   780       let container = areaNode.customizationTarget;
   781       if (isOverflowable) {
   782         container = areaNode.overflowable.getContainerFor(widgetNode);
   783       }
   785       this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null, container, true);
   787       // We remove location attributes here to make sure they're gone too when a
   788       // widget is removed from a toolbar to the palette. See bug 930950.
   789       this.removeLocationAttributes(widgetNode);
   790       // We also need to remove the panel context menu if it's there:
   791       this.ensureButtonContextMenu(widgetNode);
   792       widgetNode.removeAttribute("wrap");
   793       if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) {
   794         container.removeChild(widgetNode);
   795       } else {
   796         areaNode.toolbox.palette.appendChild(widgetNode);
   797       }
   798       this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null, container, true);
   800       if (isToolbar) {
   801         areaNode.setAttribute("currentset", gPlacements.get(aArea).join(','));
   802       }
   804       let windowCache = gSingleWrapperCache.get(window);
   805       if (windowCache) {
   806         windowCache.delete(aWidgetId);
   807       }
   808     }
   809     if (!gResetting) {
   810       this._clearPreviousUIState();
   811     }
   812   },
   814   onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) {
   815     this.insertNode(aWidgetId, aArea, aNewPosition);
   816     if (!gResetting) {
   817       this._clearPreviousUIState();
   818     }
   819   },
   821   onCustomizeEnd: function(aWindow) {
   822     this._clearPreviousUIState();
   823   },
   825   registerBuildArea: function(aArea, aNode) {
   826     // We ensure that the window is registered to have its customization data
   827     // cleaned up when unloading.
   828     let window = aNode.ownerDocument.defaultView;
   829     if (window.closed) {
   830       return;
   831     }
   832     this.registerBuildWindow(window);
   834     // Also register this build area's toolbox.
   835     if (aNode.toolbox) {
   836       gBuildWindows.get(window).add(aNode.toolbox);
   837     }
   839     if (!gBuildAreas.has(aArea)) {
   840       gBuildAreas.set(aArea, new Set());
   841     }
   843     gBuildAreas.get(aArea).add(aNode);
   845     // Give a class to all customize targets to be used for styling in Customize Mode
   846     let customizableNode = this.getCustomizeTargetForArea(aArea, window);
   847     customizableNode.classList.add("customization-target");
   848   },
   850   registerBuildWindow: function(aWindow) {
   851     if (!gBuildWindows.has(aWindow)) {
   852       gBuildWindows.set(aWindow, new Set());
   854       aWindow.addEventListener("unload", this);
   855       aWindow.addEventListener("command", this, true);
   857       this.notifyListeners("onWindowOpened", aWindow);
   858     }
   859   },
   861   unregisterBuildWindow: function(aWindow) {
   862     aWindow.removeEventListener("unload", this);
   863     aWindow.removeEventListener("command", this, true);
   864     gPanelsForWindow.delete(aWindow);
   865     gBuildWindows.delete(aWindow);
   866     gSingleWrapperCache.delete(aWindow);
   867     let document = aWindow.document;
   869     for (let [areaId, areaNodes] of gBuildAreas) {
   870       let areaProperties = gAreas.get(areaId);
   871       for (let node of areaNodes) {
   872         if (node.ownerDocument == document) {
   873           this.notifyListeners("onAreaNodeUnregistered", areaId, node.customizationTarget,
   874                                CustomizableUI.REASON_WINDOW_CLOSED);
   875           if (areaProperties.has("overflowable")) {
   876             node.overflowable.uninit();
   877             node.overflowable = null;
   878           }
   879           areaNodes.delete(node);
   880         }
   881       }
   882     }
   884     for (let [,widget] of gPalette) {
   885       widget.instances.delete(document);
   886       this.notifyListeners("onWidgetInstanceRemoved", widget.id, document);
   887     }
   889     for (let [area, areaMap] of gPendingBuildAreas) {
   890       let toDelete = [];
   891       for (let [areaNode, ] of areaMap) {
   892         if (areaNode.ownerDocument == document) {
   893           toDelete.push(areaNode);
   894         }
   895       }
   896       for (let areaNode of toDelete) {
   897         areaMap.delete(toDelete);
   898       }
   899     }
   901     this.notifyListeners("onWindowClosed", aWindow);
   902   },
   904   setLocationAttributes: function(aNode, aArea) {
   905     let props = gAreas.get(aArea);
   906     if (!props) {
   907       throw new Error("Expected area " + aArea + " to have a properties Map " +
   908                       "associated with it.");
   909     }
   911     aNode.setAttribute("cui-areatype", props.get("type") || "");
   912     let anchor = props.get("anchor");
   913     if (anchor) {
   914       aNode.setAttribute("cui-anchorid", anchor);
   915     } else {
   916       aNode.removeAttribute("cui-anchorid");
   917     }
   918   },
   920   removeLocationAttributes: function(aNode) {
   921     aNode.removeAttribute("cui-areatype");
   922     aNode.removeAttribute("cui-anchorid");
   923   },
   925   insertNode: function(aWidgetId, aArea, aPosition, isNew) {
   926     let areaNodes = gBuildAreas.get(aArea);
   927     if (!areaNodes) {
   928       return;
   929     }
   931     let placements = gPlacements.get(aArea);
   932     if (!placements) {
   933       ERROR("Could not find any placements for " + aArea +
   934             " when moving a widget.");
   935       return;
   936     }
   938     // Go through each of the nodes associated with this area and move the
   939     // widget to the requested location.
   940     for (let areaNode of areaNodes) {
   941       this.insertNodeInWindow(aWidgetId, areaNode, isNew);
   942     }
   943   },
   945   insertNodeInWindow: function(aWidgetId, aAreaNode, isNew) {
   946     let window = aAreaNode.ownerDocument.defaultView;
   947     let showInPrivateBrowsing = gPalette.has(aWidgetId)
   948                               ? gPalette.get(aWidgetId).showInPrivateBrowsing
   949                               : true;
   951     if (!showInPrivateBrowsing && PrivateBrowsingUtils.isWindowPrivate(window)) {
   952       return;
   953     }
   955     let [, widgetNode] = this.getWidgetNode(aWidgetId, window);
   956     if (!widgetNode) {
   957       ERROR("Widget '" + aWidgetId + "' not found, unable to move");
   958       return;
   959     }
   961     let areaId = aAreaNode.id;
   962     if (isNew) {
   963       this.ensureButtonContextMenu(widgetNode, aAreaNode);
   964       if (widgetNode.localName == "toolbarbutton" && areaId == CustomizableUI.AREA_PANEL) {
   965         widgetNode.setAttribute("wrap", "true");
   966       }
   967     }
   969     let [insertionContainer, nextNode] = this.findInsertionPoints(widgetNode, aAreaNode);
   970     this.insertWidgetBefore(widgetNode, nextNode, insertionContainer, areaId);
   972     if (gAreas.get(areaId).get("type") == CustomizableUI.TYPE_TOOLBAR) {
   973       aAreaNode.setAttribute("currentset", gPlacements.get(areaId).join(','));
   974     }
   975   },
   977   findInsertionPoints: function(aNode, aAreaNode) {
   978     let areaId = aAreaNode.id;
   979     let props = gAreas.get(areaId);
   981     // For overflowable toolbars, rely on them (because the work is more complicated):
   982     if (props.get("type") == CustomizableUI.TYPE_TOOLBAR && props.get("overflowable")) {
   983       return aAreaNode.overflowable.findOverflowedInsertionPoints(aNode);
   984     }
   986     let container = aAreaNode.customizationTarget;
   987     let placements = gPlacements.get(areaId);
   988     let nodeIndex = placements.indexOf(aNode.id);
   990     while (++nodeIndex < placements.length) {
   991       let nextNodeId = placements[nodeIndex];
   992       let nextNode = container.getElementsByAttribute("id", nextNodeId).item(0);
   994       if (nextNode) {
   995         return [container, nextNode];
   996       }
   997     }
   999     return [container, null];
  1000   },
  1002   insertWidgetBefore: function(aNode, aNextNode, aContainer, aArea) {
  1003     this.notifyListeners("onWidgetBeforeDOMChange", aNode, aNextNode, aContainer);
  1004     this.setLocationAttributes(aNode, aArea);
  1005     aContainer.insertBefore(aNode, aNextNode);
  1006     this.notifyListeners("onWidgetAfterDOMChange", aNode, aNextNode, aContainer);
  1007   },
  1009   handleEvent: function(aEvent) {
  1010     switch (aEvent.type) {
  1011       case "command":
  1012         if (!this._originalEventInPanel(aEvent)) {
  1013           break;
  1015         aEvent = aEvent.sourceEvent;
  1016         // Fall through
  1017       case "click":
  1018       case "keypress":
  1019         this.maybeAutoHidePanel(aEvent);
  1020         break;
  1021       case "unload":
  1022         this.unregisterBuildWindow(aEvent.currentTarget);
  1023         break;
  1025   },
  1027   _originalEventInPanel: function(aEvent) {
  1028     let e = aEvent.sourceEvent;
  1029     if (!e) {
  1030       return false;
  1032     let node = this._getPanelForNode(e.target);
  1033     if (!node) {
  1034       return false;
  1036     let win = e.view;
  1037     let panels = gPanelsForWindow.get(win);
  1038     return !!panels && panels.has(node);
  1039   },
  1041   isSpecialWidget: function(aId) {
  1042     return (aId.startsWith(kSpecialWidgetPfx) ||
  1043             aId.startsWith("separator") ||
  1044             aId.startsWith("spring") ||
  1045             aId.startsWith("spacer"));
  1046   },
  1048   ensureSpecialWidgetId: function(aId) {
  1049     let nodeType = aId.match(/spring|spacer|separator/)[0];
  1050     // If the ID we were passed isn't a generated one, generate one now:
  1051     if (nodeType == aId) {
  1052       // Ids are differentiated through a unique count suffix.
  1053       return kSpecialWidgetPfx + aId + (++gNewElementCount);
  1055     return aId;
  1056   },
  1058   createSpecialWidget: function(aId, aDocument) {
  1059     let nodeName = "toolbar" + aId.match(/spring|spacer|separator/)[0];
  1060     let node = aDocument.createElementNS(kNSXUL, nodeName);
  1061     node.id = this.ensureSpecialWidgetId(aId);
  1062     if (nodeName == "toolbarspring") {
  1063       node.flex = 1;
  1065     return node;
  1066   },
  1068   /* Find a XUL-provided widget in a window. Don't try to use this
  1069    * for an API-provided widget or a special widget.
  1070    */
  1071   findWidgetInWindow: function(aId, aWindow) {
  1072     if (!gBuildWindows.has(aWindow)) {
  1073       throw new Error("Build window not registered");
  1076     if (!aId) {
  1077       ERROR("findWidgetInWindow was passed an empty string.");
  1078       return null;
  1081     let document = aWindow.document;
  1083     // look for a node with the same id, as the node may be
  1084     // in a different toolbar.
  1085     let node = document.getElementById(aId);
  1086     if (node) {
  1087       let parent = node.parentNode;
  1088       while (parent && !(parent.customizationTarget ||
  1089                          parent == aWindow.gNavToolbox.palette)) {
  1090         parent = parent.parentNode;
  1093       if (parent) {
  1094         let nodeInArea = node.parentNode.localName == "toolbarpaletteitem" ?
  1095                          node.parentNode : node;
  1096         // Check if we're in a customization target, or in the palette:
  1097         if ((parent.customizationTarget == nodeInArea.parentNode &&
  1098              gBuildWindows.get(aWindow).has(parent.toolbox)) ||
  1099             aWindow.gNavToolbox.palette == nodeInArea.parentNode) {
  1100           // Normalize the removable attribute. For backwards compat, if
  1101           // the widget is not located in a toolbox palette then absence
  1102           // of the "removable" attribute means it is not removable.
  1103           if (!node.hasAttribute("removable")) {
  1104             // If we first see this in customization mode, it may be in the
  1105             // customization palette instead of the toolbox palette.
  1106             node.setAttribute("removable", !parent.customizationTarget);
  1108           return node;
  1113     let toolboxes = gBuildWindows.get(aWindow);
  1114     for (let toolbox of toolboxes) {
  1115       if (toolbox.palette) {
  1116         // Attempt to locate a node with a matching ID within
  1117         // the palette.
  1118         let node = toolbox.palette.getElementsByAttribute("id", aId)[0];
  1119         if (node) {
  1120           // Normalize the removable attribute. For backwards compat, this
  1121           // is optional if the widget is located in the toolbox palette,
  1122           // and defaults to *true*, unlike if it was located elsewhere.
  1123           if (!node.hasAttribute("removable")) {
  1124             node.setAttribute("removable", true);
  1126           return node;
  1130     return null;
  1131   },
  1133   buildWidget: function(aDocument, aWidget) {
  1134     if (typeof aWidget == "string") {
  1135       aWidget = gPalette.get(aWidget);
  1137     if (!aWidget) {
  1138       throw new Error("buildWidget was passed a non-widget to build.");
  1141     LOG("Building " + aWidget.id + " of type " + aWidget.type);
  1143     let node;
  1144     if (aWidget.type == "custom") {
  1145       if (aWidget.onBuild) {
  1146         node = aWidget.onBuild(aDocument);
  1148       if (!node || !(node instanceof aDocument.defaultView.XULElement))
  1149         ERROR("Custom widget with id " + aWidget.id + " does not return a valid node");
  1151     else {
  1152       if (aWidget.onBeforeCreated) {
  1153         aWidget.onBeforeCreated(aDocument);
  1155       node = aDocument.createElementNS(kNSXUL, "toolbarbutton");
  1157       node.setAttribute("id", aWidget.id);
  1158       node.setAttribute("widget-id", aWidget.id);
  1159       node.setAttribute("widget-type", aWidget.type);
  1160       if (aWidget.disabled) {
  1161         node.setAttribute("disabled", true);
  1163       node.setAttribute("removable", aWidget.removable);
  1164       node.setAttribute("overflows", aWidget.overflows);
  1165       node.setAttribute("label", this.getLocalizedProperty(aWidget, "label"));
  1166       let additionalTooltipArguments = [];
  1167       if (aWidget.shortcutId) {
  1168         let keyEl = aDocument.getElementById(aWidget.shortcutId);
  1169         if (keyEl) {
  1170           additionalTooltipArguments.push(ShortcutUtils.prettifyShortcut(keyEl));
  1171         } else {
  1172           ERROR("Key element with id '" + aWidget.shortcutId + "' for widget '" + aWidget.id +
  1173                 "' not found!");
  1177       let tooltip = this.getLocalizedProperty(aWidget, "tooltiptext", additionalTooltipArguments);
  1178       node.setAttribute("tooltiptext", tooltip);
  1179       node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional");
  1181       let commandHandler = this.handleWidgetCommand.bind(this, aWidget, node);
  1182       node.addEventListener("command", commandHandler, false);
  1183       let clickHandler = this.handleWidgetClick.bind(this, aWidget, node);
  1184       node.addEventListener("click", clickHandler, false);
  1186       // If the widget has a view, and has view showing / hiding listeners,
  1187       // hook those up to this widget.
  1188       if (aWidget.type == "view") {
  1189         LOG("Widget " + aWidget.id + " has a view. Auto-registering event handlers.");
  1190         let viewNode = aDocument.getElementById(aWidget.viewId);
  1192         if (viewNode) {
  1193           // PanelUI relies on the .PanelUI-subView class to be able to show only
  1194           // one sub-view at a time.
  1195           viewNode.classList.add("PanelUI-subView");
  1197           for (let eventName of kSubviewEvents) {
  1198             let handler = "on" + eventName;
  1199             if (typeof aWidget[handler] == "function") {
  1200               viewNode.addEventListener(eventName, aWidget[handler], false);
  1204           LOG("Widget " + aWidget.id + " showing and hiding event handlers set.");
  1205         } else {
  1206           ERROR("Could not find the view node with id: " + aWidget.viewId +
  1207                 ", for widget: " + aWidget.id + ".");
  1211       if (aWidget.onCreated) {
  1212         aWidget.onCreated(node);
  1216     aWidget.instances.set(aDocument, node);
  1217     return node;
  1218   },
  1220   getLocalizedProperty: function(aWidget, aProp, aFormatArgs, aDef) {
  1221     if (typeof aWidget == "string") {
  1222       aWidget = gPalette.get(aWidget);
  1224     if (!aWidget) {
  1225       throw new Error("getLocalizedProperty was passed a non-widget to work with.");
  1227     let def, name;
  1228     // Let widgets pass their own string identifiers or strings, so that
  1229     // we can use strings which aren't the default (in case string ids change)
  1230     // and so that non-builtin-widgets can also provide labels, tooltips, etc.
  1231     if (aWidget[aProp]) {
  1232       name = aWidget[aProp];
  1233       // By using this as the default, if a widget provides a full string rather
  1234       // than a string ID for localization, we will fall back to that string
  1235       // and return that.
  1236       def = aDef || name;
  1237     } else {
  1238       name = aWidget.id + "." + aProp;
  1239       def = aDef || "";
  1241     try {
  1242       if (Array.isArray(aFormatArgs) && aFormatArgs.length) {
  1243         return gWidgetsBundle.formatStringFromName(name, aFormatArgs,
  1244           aFormatArgs.length) || def;
  1246       return gWidgetsBundle.GetStringFromName(name) || def;
  1247     } catch(ex) {
  1248       if (!def) {
  1249         ERROR("Could not localize property '" + name + "'.");
  1252     return def;
  1253   },
  1255   handleWidgetCommand: function(aWidget, aNode, aEvent) {
  1256     LOG("handleWidgetCommand");
  1258     if (aWidget.type == "button") {
  1259       if (aWidget.onCommand) {
  1260         try {
  1261           aWidget.onCommand.call(null, aEvent);
  1262         } catch (e) {
  1263           ERROR(e);
  1265       } else {
  1266         //XXXunf Need to think this through more, and formalize.
  1267         Services.obs.notifyObservers(aNode,
  1268                                      "customizedui-widget-command",
  1269                                      aWidget.id);
  1271     } else if (aWidget.type == "view") {
  1272       let ownerWindow = aNode.ownerDocument.defaultView;
  1273       let area = this.getPlacementOfWidget(aNode.id).area;
  1274       let anchor = aNode;
  1275       if (area != CustomizableUI.AREA_PANEL) {
  1276         let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow);
  1277         if (wrapper && wrapper.anchor) {
  1278           this.hidePanelForNode(aNode);
  1279           anchor = wrapper.anchor;
  1282       ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, area);
  1284   },
  1286   handleWidgetClick: function(aWidget, aNode, aEvent) {
  1287     LOG("handleWidgetClick");
  1288     if (aWidget.onClick) {
  1289       try {
  1290         aWidget.onClick.call(null, aEvent);
  1291       } catch(e) {
  1292         Cu.reportError(e);
  1294     } else {
  1295       //XXXunf Need to think this through more, and formalize.
  1296       Services.obs.notifyObservers(aNode, "customizedui-widget-click", aWidget.id);
  1298   },
  1300   _getPanelForNode: function(aNode) {
  1301     let panel = aNode;
  1302     while (panel && panel.localName != "panel")
  1303       panel = panel.parentNode;
  1304     return panel;
  1305   },
  1307   /*
  1308    * If people put things in the panel which need more than single-click interaction,
  1309    * we don't want to close it. Right now we check for text inputs and menu buttons.
  1310    * We also check for being outside of any toolbaritem/toolbarbutton, ie on a blank
  1311    * part of the menu.
  1312    */
  1313   _isOnInteractiveElement: function(aEvent) {
  1314     function getMenuPopupForDescendant(aNode) {
  1315       let lastPopup = null;
  1316       while (aNode && aNode.parentNode &&
  1317              aNode.parentNode.localName.startsWith("menu")) {
  1318         lastPopup = aNode.localName == "menupopup" ? aNode : lastPopup;
  1319         aNode = aNode.parentNode;
  1321       return lastPopup;
  1324     let target = aEvent.originalTarget;
  1325     let panel = this._getPanelForNode(aEvent.currentTarget);
  1326     // This can happen in e.g. customize mode. If there's no panel,
  1327     // there's clearly nothing for us to close; pretend we're interactive.
  1328     if (!panel) {
  1329       return true;
  1331     // We keep track of:
  1332     // whether we're in an input container (text field)
  1333     let inInput = false;
  1334     // whether we're in a popup/context menu
  1335     let inMenu = false;
  1336     // whether we're in a toolbarbutton/toolbaritem
  1337     let inItem = false;
  1338     // whether the current menuitem has a valid closemenu attribute
  1339     let menuitemCloseMenu = "auto";
  1340     // whether the toolbarbutton/item has a valid closemenu attribute.
  1341     let closemenu = "auto";
  1343     // While keeping track of that, we go from the original target back up,
  1344     // to the panel if we have to. We bail as soon as we find an input,
  1345     // a toolbarbutton/item, or the panel:
  1346     while (true && target) {
  1347       let tagName = target.localName;
  1348       inInput = tagName == "input" || tagName == "textbox";
  1349       inItem = tagName == "toolbaritem" || tagName == "toolbarbutton";
  1350       let isMenuItem = tagName == "menuitem";
  1351       inMenu = inMenu || isMenuItem;
  1352       if (inItem && target.hasAttribute("closemenu")) {
  1353         let closemenuVal = target.getAttribute("closemenu");
  1354         closemenu = (closemenuVal == "single" || closemenuVal == "none") ?
  1355                     closemenuVal : "auto";
  1358       if (isMenuItem && target.hasAttribute("closemenu")) {
  1359         let closemenuVal = target.getAttribute("closemenu");
  1360         menuitemCloseMenu = (closemenuVal == "single" || closemenuVal == "none") ?
  1361                             closemenuVal : "auto";
  1363       // This isn't in the loop condition because we want to break before
  1364       // changing |target| if any of these conditions are true
  1365       if (inInput || inItem || target == panel) {
  1366         break;
  1368       // We need specific code for popups: the item on which they were invoked
  1369       // isn't necessarily in their parentNode chain:
  1370       if (isMenuItem) {
  1371         let topmostMenuPopup = getMenuPopupForDescendant(target);
  1372         target = (topmostMenuPopup && topmostMenuPopup.triggerNode) ||
  1373                  target.parentNode;
  1374       } else {
  1375         target = target.parentNode;
  1378     // If the user clicked a menu item...
  1379     if (inMenu) {
  1380       // We care if we're in an input also,
  1381       // or if the user specified closemenu!="auto":
  1382       if (inInput || menuitemCloseMenu != "auto") {
  1383         return true;
  1385       // Otherwise, we're probably fine to close the panel
  1386       return false;
  1388     // If we're not in a menu, and we *are* in a type="menu" toolbarbutton,
  1389     // we'll now interact with the menu
  1390     if (inItem && target.getAttribute("type") == "menu") {
  1391       return true;
  1393     // If we're not in a menu, and we *are* in a type="menu-button" toolbarbutton,
  1394     // it depends whether we're in the dropmarker or the 'real' button:
  1395     if (inItem && target.getAttribute("type") == "menu-button") {
  1396       // 'real' button (which has a single action):
  1397       if (target.getAttribute("anonid") == "button") {
  1398         return closemenu != "none";
  1400       // otherwise, this is the outer button, and the user will now
  1401       // interact with the menu:
  1402       return true;
  1404     return inInput || !inItem;
  1405   },
  1407   hidePanelForNode: function(aNode) {
  1408     let panel = this._getPanelForNode(aNode);
  1409     if (panel) {
  1410       panel.hidePopup();
  1412   },
  1414   maybeAutoHidePanel: function(aEvent) {
  1415     if (aEvent.type == "keypress") {
  1416       if (aEvent.keyCode != aEvent.DOM_VK_RETURN) {
  1417         return;
  1419       // If the user hit enter/return, we don't check preventDefault - it makes sense
  1420       // that this was prevented, but we probably still want to close the panel.
  1421       // If consumers don't want this to happen, they should specify the closemenu
  1422       // attribute.
  1424     } else if (aEvent.type != "command") { // mouse events:
  1425       if (aEvent.defaultPrevented || aEvent.button != 0) {
  1426         return;
  1428       let isInteractive = this._isOnInteractiveElement(aEvent);
  1429       LOG("maybeAutoHidePanel: interactive ? " + isInteractive);
  1430       if (isInteractive) {
  1431         return;
  1435     // We can't use event.target because we might have passed a panelview
  1436     // anonymous content boundary as well, and so target points to the
  1437     // panelmultiview in that case. Unfortunately, this means we get
  1438     // anonymous child nodes instead of the real ones, so looking for the 
  1439     // 'stoooop, don't close me' attributes is more involved.
  1440     let target = aEvent.originalTarget;
  1441     let closemenu = "auto";
  1442     let widgetType = "button";
  1443     while (target.parentNode && target.localName != "panel") {
  1444       closemenu = target.getAttribute("closemenu");
  1445       widgetType = target.getAttribute("widget-type");
  1446       if (closemenu == "none" || closemenu == "single" ||
  1447           widgetType == "view") {
  1448         break;
  1450       target = target.parentNode;
  1452     if (closemenu == "none" || widgetType == "view") {
  1453       return;
  1456     if (closemenu == "single") {
  1457       let panel = this._getPanelForNode(target);
  1458       let multiview = panel.querySelector("panelmultiview");
  1459       if (multiview.showingSubView) {
  1460         multiview.showMainView();
  1461         return;
  1465     // If we get here, we can actually hide the popup:
  1466     this.hidePanelForNode(aEvent.target);
  1467   },
  1469   getUnusedWidgets: function(aWindowPalette) {
  1470     let window = aWindowPalette.ownerDocument.defaultView;
  1471     let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
  1472     // We use a Set because there can be overlap between the widgets in
  1473     // gPalette and the items in the palette, especially after the first
  1474     // customization, since programmatically generated widgets will remain
  1475     // in the toolbox palette.
  1476     let widgets = new Set();
  1478     // It's possible that some widgets have been defined programmatically and
  1479     // have not been overlayed into the palette. We can find those inside
  1480     // gPalette.
  1481     for (let [id, widget] of gPalette) {
  1482       if (!widget.currentArea) {
  1483         if (widget.showInPrivateBrowsing || !isWindowPrivate) {
  1484           widgets.add(id);
  1489     LOG("Iterating the actual nodes of the window palette");
  1490     for (let node of aWindowPalette.children) {
  1491       LOG("In palette children: " + node.id);
  1492       if (node.id && !this.getPlacementOfWidget(node.id)) {
  1493         widgets.add(node.id);
  1497     return [...widgets];
  1498   },
  1500   getPlacementOfWidget: function(aWidgetId, aOnlyRegistered, aDeadAreas) {
  1501     if (aOnlyRegistered && !this.widgetExists(aWidgetId)) {
  1502       return null;
  1505     for (let [area, placements] of gPlacements) {
  1506       if (!gAreas.has(area) && !aDeadAreas) {
  1507         continue;
  1509       let index = placements.indexOf(aWidgetId);
  1510       if (index != -1) {
  1511         return { area: area, position: index };
  1515     return null;
  1516   },
  1518   widgetExists: function(aWidgetId) {
  1519     if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) {
  1520       return true;
  1523     // Destroyed API widgets are in gSeenWidgets, but not in gPalette:
  1524     if (gSeenWidgets.has(aWidgetId)) {
  1525       return false;
  1528     // We're assuming XUL widgets always exist, as it's much harder to check,
  1529     // and checking would be much more error prone.
  1530     return true;
  1531   },
  1533   addWidgetToArea: function(aWidgetId, aArea, aPosition, aInitialAdd) {
  1534     if (!gAreas.has(aArea)) {
  1535       throw new Error("Unknown customization area: " + aArea);
  1538     // Hack: don't want special widgets in the panel (need to check here as well
  1539     // as in canWidgetMoveToArea because the menu panel is lazy):
  1540     if (gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL &&
  1541         this.isSpecialWidget(aWidgetId)) {
  1542       return;
  1545     // If this is a lazy area that hasn't been restored yet, we can't yet modify
  1546     // it - would would at least like to add to it. So we keep track of it in
  1547     // gFuturePlacements,  and use that to add it when restoring the area. We
  1548     // throw away aPosition though, as that can only be bogus if the area hasn't
  1549     // yet been restorted (caller can't possibly know where its putting the
  1550     // widget in relation to other widgets).
  1551     if (this.isAreaLazy(aArea)) {
  1552       gFuturePlacements.get(aArea).add(aWidgetId);
  1553       return;
  1556     if (this.isSpecialWidget(aWidgetId)) {
  1557       aWidgetId = this.ensureSpecialWidgetId(aWidgetId);
  1560     let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
  1561     if (oldPlacement && oldPlacement.area == aArea) {
  1562       this.moveWidgetWithinArea(aWidgetId, aPosition);
  1563       return;
  1566     // Do nothing if the widget is not allowed to move to the target area.
  1567     if (!this.canWidgetMoveToArea(aWidgetId, aArea)) {
  1568       return;
  1571     if (oldPlacement) {
  1572       this.removeWidgetFromArea(aWidgetId);
  1575     if (!gPlacements.has(aArea)) {
  1576       gPlacements.set(aArea, [aWidgetId]);
  1577       aPosition = 0;
  1578     } else {
  1579       let placements = gPlacements.get(aArea);
  1580       if (typeof aPosition != "number") {
  1581         aPosition = placements.length;
  1583       if (aPosition < 0) {
  1584         aPosition = 0;
  1586       placements.splice(aPosition, 0, aWidgetId);
  1589     let widget = gPalette.get(aWidgetId);
  1590     if (widget) {
  1591       widget.currentArea = aArea;
  1592       widget.currentPosition = aPosition;
  1595     // We initially set placements with addWidgetToArea, so in that case
  1596     // we don't consider the area "dirtied".
  1597     if (!aInitialAdd) {
  1598       gDirtyAreaCache.add(aArea);
  1601     gDirty = true;
  1602     this.saveState();
  1604     this.notifyListeners("onWidgetAdded", aWidgetId, aArea, aPosition);
  1605   },
  1607   removeWidgetFromArea: function(aWidgetId) {
  1608     let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
  1609     if (!oldPlacement) {
  1610       return;
  1613     if (!this.isWidgetRemovable(aWidgetId)) {
  1614       return;
  1617     let placements = gPlacements.get(oldPlacement.area);
  1618     let position = placements.indexOf(aWidgetId);
  1619     if (position != -1) {
  1620       placements.splice(position, 1);
  1623     let widget = gPalette.get(aWidgetId);
  1624     if (widget) {
  1625       widget.currentArea = null;
  1626       widget.currentPosition = null;
  1629     gDirty = true;
  1630     this.saveState();
  1631     gDirtyAreaCache.add(oldPlacement.area);
  1633     this.notifyListeners("onWidgetRemoved", aWidgetId, oldPlacement.area);
  1634   },
  1636   moveWidgetWithinArea: function(aWidgetId, aPosition) {
  1637     let oldPlacement = this.getPlacementOfWidget(aWidgetId);
  1638     if (!oldPlacement) {
  1639       return;
  1642     let placements = gPlacements.get(oldPlacement.area);
  1643     if (typeof aPosition != "number") {
  1644       aPosition = placements.length;
  1645     } else if (aPosition < 0) {
  1646       aPosition = 0;
  1647     } else if (aPosition > placements.length) {
  1648       aPosition = placements.length;
  1651     let widget = gPalette.get(aWidgetId);
  1652     if (widget) {
  1653       widget.currentPosition = aPosition;
  1654       widget.currentArea = oldPlacement.area;
  1657     if (aPosition == oldPlacement.position) {
  1658       return;
  1661     placements.splice(oldPlacement.position, 1);
  1662     // If we just removed the item from *before* where it is now added,
  1663     // we need to compensate the position offset for that:
  1664     if (oldPlacement.position < aPosition) {
  1665       aPosition--;
  1667     placements.splice(aPosition, 0, aWidgetId);
  1669     gDirty = true;
  1670     gDirtyAreaCache.add(oldPlacement.area);
  1672     this.saveState();
  1674     this.notifyListeners("onWidgetMoved", aWidgetId, oldPlacement.area,
  1675                          oldPlacement.position, aPosition);
  1676   },
  1678   // Note that this does not populate gPlacements, which is done lazily so that
  1679   // the legacy state can be migrated, which is only available once a browser
  1680   // window is openned.
  1681   // The panel area is an exception here, since it has no legacy state and is 
  1682   // built lazily - and therefore wouldn't otherwise result in restoring its
  1683   // state immediately when a browser window opens, which is important for
  1684   // other consumers of this API.
  1685   loadSavedState: function() {
  1686     let state = null;
  1687     try {
  1688       state = Services.prefs.getCharPref(kPrefCustomizationState);
  1689     } catch (e) {
  1690       LOG("No saved state found");
  1691       // This will fail if nothing has been customized, so silently fall back to
  1692       // the defaults.
  1695     if (!state) {
  1696       return;
  1698     try {
  1699       gSavedState = JSON.parse(state);
  1700       if (typeof gSavedState != "object" || gSavedState === null) {
  1701         throw "Invalid saved state";
  1703     } catch(e) {
  1704       Services.prefs.clearUserPref(kPrefCustomizationState);
  1705       gSavedState = {};
  1706       LOG("Error loading saved UI customization state, falling back to defaults.");
  1709     if (!("placements" in gSavedState)) {
  1710       gSavedState.placements = {};
  1713     gSeenWidgets = new Set(gSavedState.seen || []);
  1714     gDirtyAreaCache = new Set(gSavedState.dirtyAreaCache || []);
  1715     gNewElementCount = gSavedState.newElementCount || 0;
  1716   },
  1718   restoreStateForArea: function(aArea, aLegacyState) {
  1719     let placementsPreexisted = gPlacements.has(aArea);
  1721     this.beginBatchUpdate();
  1722     try {
  1723       gRestoring = true;
  1725       let restored = false;
  1726       if (placementsPreexisted) {
  1727         LOG("Restoring " + aArea + " from pre-existing placements");
  1728         for (let [position, id] in Iterator(gPlacements.get(aArea))) {
  1729           this.moveWidgetWithinArea(id, position);
  1731         gDirty = false;
  1732         restored = true;
  1733       } else {
  1734         gPlacements.set(aArea, []);
  1737       if (!restored && gSavedState && aArea in gSavedState.placements) {
  1738         LOG("Restoring " + aArea + " from saved state");
  1739         let placements = gSavedState.placements[aArea];
  1740         for (let id of placements)
  1741           this.addWidgetToArea(id, aArea);
  1742         gDirty = false;
  1743         restored = true;
  1746       if (!restored && aLegacyState) {
  1747         LOG("Restoring " + aArea + " from legacy state");
  1748         for (let id of aLegacyState)
  1749           this.addWidgetToArea(id, aArea);
  1750         // Don't override dirty state, to ensure legacy state is saved here and
  1751         // therefore only used once.
  1752         restored = true;
  1755       if (!restored) {
  1756         LOG("Restoring " + aArea + " from default state");
  1757         let defaults = gAreas.get(aArea).get("defaultPlacements");
  1758         if (defaults) {
  1759           for (let id of defaults)
  1760             this.addWidgetToArea(id, aArea, null, true);
  1762         gDirty = false;
  1765       // Finally, add widgets to the area that were added before the it was able
  1766       // to be restored. This can occur when add-ons register widgets for a
  1767       // lazily-restored area before it's been restored.
  1768       if (gFuturePlacements.has(aArea)) {
  1769         for (let id of gFuturePlacements.get(aArea))
  1770           this.addWidgetToArea(id, aArea);
  1773       LOG("Placements for " + aArea + ":\n\t" + gPlacements.get(aArea).join("\n\t"));
  1775       gRestoring = false;
  1776     } finally {
  1777       this.endBatchUpdate();
  1779   },
  1781   saveState: function() {
  1782     if (gInBatchStack || !gDirty) {
  1783       return;
  1785     let state = { placements: gPlacements,
  1786                   seen: gSeenWidgets,
  1787                   dirtyAreaCache: gDirtyAreaCache,
  1788                   newElementCount: gNewElementCount };
  1790     LOG("Saving state.");
  1791     let serialized = JSON.stringify(state, this.serializerHelper);
  1792     LOG("State saved as: " + serialized);
  1793     Services.prefs.setCharPref(kPrefCustomizationState, serialized);
  1794     gDirty = false;
  1795   },
  1797   serializerHelper: function(aKey, aValue) {
  1798     if (typeof aValue == "object" && aValue.constructor.name == "Map") {
  1799       let result = {};
  1800       for (let [mapKey, mapValue] of aValue)
  1801         result[mapKey] = mapValue;
  1802       return result;
  1805     if (typeof aValue == "object" && aValue.constructor.name == "Set") {
  1806       return [...aValue];
  1809     return aValue;
  1810   },
  1812   beginBatchUpdate: function() {
  1813     gInBatchStack++;
  1814   },
  1816   endBatchUpdate: function(aForceDirty) {
  1817     gInBatchStack--;
  1818     if (aForceDirty === true) {
  1819       gDirty = true;
  1821     if (gInBatchStack == 0) {
  1822       this.saveState();
  1823     } else if (gInBatchStack < 0) {
  1824       throw new Error("The batch editing stack should never reach a negative number.");
  1826   },
  1828   addListener: function(aListener) {
  1829     gListeners.add(aListener);
  1830   },
  1832   removeListener: function(aListener) {
  1833     if (aListener == this) {
  1834       return;
  1837     gListeners.delete(aListener);
  1838   },
  1840   notifyListeners: function(aEvent, ...aArgs) {
  1841     if (gRestoring) {
  1842       return;
  1845     for (let listener of gListeners) {
  1846       try {
  1847         if (typeof listener[aEvent] == "function") {
  1848           listener[aEvent].apply(listener, aArgs);
  1850       } catch (e) {
  1851         ERROR(e + " -- " + e.fileName + ":" + e.lineNumber);
  1854   },
  1856   _dispatchToolboxEventToWindow: function(aEventType, aDetails, aWindow) {
  1857     let evt = new aWindow.CustomEvent(aEventType, {
  1858       bubbles: true,
  1859       cancelable: true,
  1860       detail: aDetails
  1861     });
  1862     aWindow.gNavToolbox.dispatchEvent(evt);
  1863   },
  1865   dispatchToolboxEvent: function(aEventType, aDetails={}, aWindow=null) {
  1866     if (aWindow) {
  1867       return this._dispatchToolboxEventToWindow(aEventType, aDetails, aWindow);
  1869     for (let [win, ] of gBuildWindows) {
  1870       this._dispatchToolboxEventToWindow(aEventType, aDetails, win);
  1872   },
  1874   createWidget: function(aProperties) {
  1875     let widget = this.normalizeWidget(aProperties, CustomizableUI.SOURCE_EXTERNAL);
  1876     //XXXunf This should probably throw.
  1877     if (!widget) {
  1878       return;
  1881     gPalette.set(widget.id, widget);
  1883     // Clear our caches:
  1884     gGroupWrapperCache.delete(widget.id);
  1885     for (let [win, ] of gBuildWindows) {
  1886       let cache = gSingleWrapperCache.get(win);
  1887       if (cache) {
  1888         cache.delete(widget.id);
  1892     this.notifyListeners("onWidgetCreated", widget.id);
  1894     if (widget.defaultArea) {
  1895       let area = gAreas.get(widget.defaultArea);
  1896       //XXXgijs this won't have any effect for legacy items. Sort of OK because
  1897       // consumers can modify currentset? Maybe?
  1898       if (area.has("defaultPlacements")) {
  1899         area.get("defaultPlacements").push(widget.id);
  1900       } else {
  1901         area.set("defaultPlacements", [widget.id]);
  1905     // Look through previously saved state to see if we're restoring a widget.
  1906     let seenAreas = new Set();
  1907     let widgetMightNeedAutoAdding = true;
  1908     for (let [area, placements] of gPlacements) {
  1909       seenAreas.add(area);
  1910       let areaIsRegistered = gAreas.has(area);
  1911       let index = gPlacements.get(area).indexOf(widget.id);
  1912       if (index != -1) {
  1913         widgetMightNeedAutoAdding = false;
  1914         if (areaIsRegistered) {
  1915           widget.currentArea = area;
  1916           widget.currentPosition = index;
  1918         break;
  1922     // Also look at saved state data directly in areas that haven't yet been
  1923     // restored. Can't rely on this for restored areas, as they may have
  1924     // changed.
  1925     if (widgetMightNeedAutoAdding && gSavedState) {
  1926       for (let area of Object.keys(gSavedState.placements)) {
  1927         if (seenAreas.has(area)) {
  1928           continue;
  1931         let areaIsRegistered = gAreas.has(area);
  1932         let index = gSavedState.placements[area].indexOf(widget.id);
  1933         if (index != -1) {
  1934           widgetMightNeedAutoAdding = false;
  1935           if (areaIsRegistered) {
  1936             widget.currentArea = area;
  1937             widget.currentPosition = index;
  1939           break;
  1944     // If we're restoring the widget to it's old placement, fire off the
  1945     // onWidgetAdded event - our own handler will take care of adding it to
  1946     // any build areas.
  1947     if (widget.currentArea) {
  1948       this.notifyListeners("onWidgetAdded", widget.id, widget.currentArea,
  1949                            widget.currentPosition);
  1950     } else if (widgetMightNeedAutoAdding) {
  1951       let autoAdd = true;
  1952       try {
  1953         autoAdd = Services.prefs.getBoolPref(kPrefCustomizationAutoAdd);
  1954       } catch (e) {}
  1956       // If the widget doesn't have an existing placement, and it hasn't been
  1957       // seen before, then add it to its default area so it can be used.
  1958       // If the widget is not removable, we *have* to add it to its default
  1959       // area here.
  1960       let canBeAutoAdded = autoAdd && !gSeenWidgets.has(widget.id);
  1961       if (!widget.currentArea && (!widget.removable || canBeAutoAdded)) {
  1962         this.beginBatchUpdate();
  1963         try {
  1964           gSeenWidgets.add(widget.id);
  1966           if (widget.defaultArea) {
  1967             if (this.isAreaLazy(widget.defaultArea)) {
  1968               gFuturePlacements.get(widget.defaultArea).add(widget.id);
  1969             } else {
  1970               this.addWidgetToArea(widget.id, widget.defaultArea);
  1973         } finally {
  1974           this.endBatchUpdate(true);
  1979     this.notifyListeners("onWidgetAfterCreation", widget.id, widget.currentArea);
  1980     return widget.id;
  1981   },
  1983   createBuiltinWidget: function(aData) {
  1984     // This should only ever be called on startup, before any windows are
  1985     // opened - so we know there's no build areas to handle. Also, builtin
  1986     // widgets are expected to be (mostly) static, so shouldn't affect the
  1987     // current placement settings.
  1988     let widget = this.normalizeWidget(aData, CustomizableUI.SOURCE_BUILTIN);
  1989     if (!widget) {
  1990       ERROR("Error creating builtin widget: " + aData.id);
  1991       return;
  1994     LOG("Creating built-in widget with id: " + widget.id);
  1995     gPalette.set(widget.id, widget);
  1996   },
  1998   // Returns true if the area will eventually lazily restore (but hasn't yet).
  1999   isAreaLazy: function(aArea) {
  2000     if (gPlacements.has(aArea)) {
  2001       return false;
  2003     return gAreas.get(aArea).has("legacy");
  2004   },
  2006   //XXXunf Log some warnings here, when the data provided isn't up to scratch.
  2007   normalizeWidget: function(aData, aSource) {
  2008     let widget = {
  2009       implementation: aData,
  2010       source: aSource || "addon",
  2011       instances: new Map(),
  2012       currentArea: null,
  2013       removable: true,
  2014       overflows: true,
  2015       defaultArea: null,
  2016       shortcutId: null,
  2017       tooltiptext: null,
  2018       showInPrivateBrowsing: true,
  2019     };
  2021     if (typeof aData.id != "string" || !/^[a-z0-9-_]{1,}$/i.test(aData.id)) {
  2022       ERROR("Given an illegal id in normalizeWidget: " + aData.id);
  2023       return null;
  2026     delete widget.implementation.currentArea;
  2027     widget.implementation.__defineGetter__("currentArea", function() widget.currentArea);
  2029     const kReqStringProps = ["id"];
  2030     for (let prop of kReqStringProps) {
  2031       if (typeof aData[prop] != "string") {
  2032         ERROR("Missing required property '" + prop + "' in normalizeWidget: "
  2033               + aData.id);
  2034         return null;
  2036       widget[prop] = aData[prop];
  2039     const kOptStringProps = ["label", "tooltiptext", "shortcutId"];
  2040     for (let prop of kOptStringProps) {
  2041       if (typeof aData[prop] == "string") {
  2042         widget[prop] = aData[prop];
  2046     const kOptBoolProps = ["removable", "showInPrivateBrowsing", "overflows"];
  2047     for (let prop of kOptBoolProps) {
  2048       if (typeof aData[prop] == "boolean") {
  2049         widget[prop] = aData[prop];
  2053     if (aData.defaultArea && gAreas.has(aData.defaultArea)) {
  2054       widget.defaultArea = aData.defaultArea;
  2055     } else if (!widget.removable) {
  2056       ERROR("Widget '" + widget.id + "' is not removable but does not specify " +
  2057             "a valid defaultArea. That's not possible; it must specify a " +
  2058             "valid defaultArea as well.");
  2059       return null;
  2062     if ("type" in aData && gSupportedWidgetTypes.has(aData.type)) {
  2063       widget.type = aData.type;
  2064     } else {
  2065       widget.type = "button";
  2068     widget.disabled = aData.disabled === true;
  2070     this.wrapWidgetEventHandler("onBeforeCreated", widget);
  2071     this.wrapWidgetEventHandler("onClick", widget);
  2072     this.wrapWidgetEventHandler("onCreated", widget);
  2074     if (widget.type == "button") {
  2075       widget.onCommand = typeof aData.onCommand == "function" ?
  2076                            aData.onCommand :
  2077                            null;
  2078     } else if (widget.type == "view") {
  2079       if (typeof aData.viewId != "string") {
  2080         ERROR("Expected a string for widget " + widget.id + " viewId, but got "
  2081               + aData.viewId);
  2082         return null;
  2084       widget.viewId = aData.viewId;
  2086       this.wrapWidgetEventHandler("onViewShowing", widget);
  2087       this.wrapWidgetEventHandler("onViewHiding", widget);
  2088     } else if (widget.type == "custom") {
  2089       this.wrapWidgetEventHandler("onBuild", widget);
  2092     if (gPalette.has(widget.id)) {
  2093       return null;
  2096     return widget;
  2097   },
  2099   wrapWidgetEventHandler: function(aEventName, aWidget) {
  2100     if (typeof aWidget.implementation[aEventName] != "function") {
  2101       aWidget[aEventName] = null;
  2102       return;
  2104     aWidget[aEventName] = function(...aArgs) {
  2105       // Wrap inside a try...catch to properly log errors, until bug 862627 is
  2106       // fixed, which in turn might help bug 503244.
  2107       try {
  2108         // Don't copy the function to the normalized widget object, instead
  2109         // keep it on the original object provided to the API so that
  2110         // additional methods can be implemented and used by the event
  2111         // handlers.
  2112         return aWidget.implementation[aEventName].apply(aWidget.implementation,
  2113                                                         aArgs);
  2114       } catch (e) {
  2115         Cu.reportError(e);
  2117     };
  2118   },
  2120   destroyWidget: function(aWidgetId) {
  2121     let widget = gPalette.get(aWidgetId);
  2122     if (!widget) {
  2123       gGroupWrapperCache.delete(aWidgetId);
  2124       for (let [window, ] of gBuildWindows) {
  2125         let windowCache = gSingleWrapperCache.get(window);
  2126         if (windowCache) {
  2127           windowCache.delete(aWidgetId);
  2130       return;
  2133     // Remove it from the default placements of an area if it was added there:
  2134     if (widget.defaultArea) {
  2135       let area = gAreas.get(widget.defaultArea);
  2136       if (area) {
  2137         let defaultPlacements = area.get("defaultPlacements");
  2138         // We can assume this is present because if a widget has a defaultArea,
  2139         // we automatically create a defaultPlacements array for that area.
  2140         let widgetIndex = defaultPlacements.indexOf(aWidgetId);
  2141         if (widgetIndex != -1) {
  2142           defaultPlacements.splice(widgetIndex, 1);
  2147     // This will not remove the widget from gPlacements - we want to keep the
  2148     // setting so the widget gets put back in it's old position if/when it
  2149     // returns.
  2150     for (let [window, ] of gBuildWindows) {
  2151       let windowCache = gSingleWrapperCache.get(window);
  2152       if (windowCache) {
  2153         windowCache.delete(aWidgetId);
  2155       let widgetNode = window.document.getElementById(aWidgetId) ||
  2156                        window.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0];
  2157       if (widgetNode) {
  2158         let container = widgetNode.parentNode
  2159         this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null,
  2160                              container, true);
  2161         widgetNode.remove();
  2162         this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null,
  2163                              container, true);
  2165       if (widget.type == "view") {
  2166         let viewNode = window.document.getElementById(widget.viewId);
  2167         if (viewNode) {
  2168           for (let eventName of kSubviewEvents) {
  2169             let handler = "on" + eventName;
  2170             if (typeof widget[handler] == "function") {
  2171               viewNode.removeEventListener(eventName, widget[handler], false);
  2178     gPalette.delete(aWidgetId);
  2179     gGroupWrapperCache.delete(aWidgetId);
  2181     this.notifyListeners("onWidgetDestroyed", aWidgetId);
  2182   },
  2184   getCustomizeTargetForArea: function(aArea, aWindow) {
  2185     let buildAreaNodes = gBuildAreas.get(aArea);
  2186     if (!buildAreaNodes) {
  2187       return null;
  2190     for (let node of buildAreaNodes) {
  2191       if (node.ownerDocument.defaultView === aWindow) {
  2192         return node.customizationTarget ? node.customizationTarget : node;
  2196     return null;
  2197   },
  2199   reset: function() {
  2200     gResetting = true;
  2201     this._resetUIState();
  2203     // Rebuild each registered area (across windows) to reflect the state that
  2204     // was reset above.
  2205     this._rebuildRegisteredAreas();
  2207     gResetting = false;
  2208   },
  2210   _resetUIState: function() {
  2211     try {
  2212       gUIStateBeforeReset.drawInTitlebar = Services.prefs.getBoolPref(kPrefDrawInTitlebar);
  2213       gUIStateBeforeReset.uiCustomizationState = Services.prefs.getCharPref(kPrefCustomizationState);
  2214     } catch(e) { }
  2216     this._resetExtraToolbars();
  2218     Services.prefs.clearUserPref(kPrefCustomizationState);
  2219     Services.prefs.clearUserPref(kPrefDrawInTitlebar);
  2220     LOG("State reset");
  2222     // Reset placements to make restoring default placements possible.
  2223     gPlacements = new Map();
  2224     gDirtyAreaCache = new Set();
  2225     gSeenWidgets = new Set();
  2226     // Clear the saved state to ensure that defaults will be used.
  2227     gSavedState = null;
  2228     // Restore the state for each area to its defaults
  2229     for (let [areaId,] of gAreas) {
  2230       this.restoreStateForArea(areaId);
  2232   },
  2234   _resetExtraToolbars: function(aFilter = null) {
  2235     let firstWindow = true; // Only need to unregister and persist once
  2236     for (let [win, ] of gBuildWindows) {
  2237       let toolbox = win.gNavToolbox;
  2238       for (let child of toolbox.children) {
  2239         let matchesFilter = !aFilter || aFilter == child.id;
  2240         if (child.hasAttribute("customindex") && matchesFilter) {
  2241           let toolbarId = "toolbar" + child.getAttribute("customindex");
  2242           toolbox.toolbarset.removeAttribute(toolbarId);
  2243           if (firstWindow) {
  2244             win.document.persist(toolbox.toolbarset.id, toolbarId);
  2245             // We have to unregister it properly to ensure we don't kill
  2246             // XUL widgets which might be in here
  2247             this.unregisterArea(child.id, true);
  2249           child.remove();
  2252       firstWindow = false;
  2254   },
  2256   _rebuildRegisteredAreas: function() {
  2257     for (let [areaId, areaNodes] of gBuildAreas) {
  2258       let placements = gPlacements.get(areaId);
  2259       let isFirstChangedToolbar = true;
  2260       for (let areaNode of areaNodes) {
  2261         this.buildArea(areaId, placements, areaNode);
  2263         let area = gAreas.get(areaId);
  2264         if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) {
  2265           let defaultCollapsed = area.get("defaultCollapsed");
  2266           let win = areaNode.ownerDocument.defaultView;
  2267           if (defaultCollapsed !== null) {
  2268             win.setToolbarVisibility(areaNode, !defaultCollapsed, isFirstChangedToolbar);
  2271         isFirstChangedToolbar = false;
  2274   },
  2276   /**
  2277    * Undoes a previous reset, restoring the state of the UI to the state prior to the reset.
  2278    */
  2279   undoReset: function() {
  2280     if (gUIStateBeforeReset.uiCustomizationState == null ||
  2281         gUIStateBeforeReset.drawInTitlebar == null) {
  2282       return;
  2284     gUndoResetting = true;
  2286     let uiCustomizationState = gUIStateBeforeReset.uiCustomizationState;
  2287     let drawInTitlebar = gUIStateBeforeReset.drawInTitlebar;
  2289     // Need to clear the previous state before setting the prefs
  2290     // because pref observers may check if there is a previous UI state.
  2291     this._clearPreviousUIState();
  2293     Services.prefs.setCharPref(kPrefCustomizationState, uiCustomizationState);
  2294     Services.prefs.setBoolPref(kPrefDrawInTitlebar, drawInTitlebar);
  2295     this.loadSavedState();
  2296     // If the user just customizes toolbar/titlebar visibility, gSavedState will be null
  2297     // and we don't need to do anything else here:
  2298     if (gSavedState) {
  2299       for (let areaId of Object.keys(gSavedState.placements)) {
  2300         let placements = gSavedState.placements[areaId];
  2301         gPlacements.set(areaId, placements);
  2303       this._rebuildRegisteredAreas();
  2306     gUndoResetting = false;
  2307   },
  2309   _clearPreviousUIState: function() {
  2310     Object.getOwnPropertyNames(gUIStateBeforeReset).forEach((prop) => {
  2311       gUIStateBeforeReset[prop] = null;
  2312     });
  2313   },
  2315   removeExtraToolbar: function(aToolbarId) {
  2316     this._resetExtraToolbars(aToolbarId);
  2317   },
  2319   /**
  2320    * @param {String|Node} aWidget - widget ID or a widget node (preferred for performance).
  2321    * @return {Boolean} whether the widget is removable
  2322    */
  2323   isWidgetRemovable: function(aWidget) {
  2324     let widgetId;
  2325     let widgetNode;
  2326     if (typeof aWidget == "string") {
  2327       widgetId = aWidget;
  2328     } else {
  2329       widgetId = aWidget.id;
  2330       widgetNode = aWidget;
  2332     let provider = this.getWidgetProvider(widgetId);
  2334     if (provider == CustomizableUI.PROVIDER_API) {
  2335       return gPalette.get(widgetId).removable;
  2338     if (provider == CustomizableUI.PROVIDER_XUL) {
  2339       if (gBuildWindows.size == 0) {
  2340         // We don't have any build windows to look at, so just assume for now
  2341         // that its removable.
  2342         return true;
  2345       if (!widgetNode) {
  2346         // Pick any of the build windows to look at.
  2347         let [window,] = [...gBuildWindows][0];
  2348         [, widgetNode] = this.getWidgetNode(widgetId, window);
  2350       // If we don't have a node, we assume it's removable. This can happen because
  2351       // getWidgetProvider returns PROVIDER_XUL by default, but this will also happen
  2352       // for API-provided widgets which have been destroyed.
  2353       if (!widgetNode) {
  2354         return true;
  2356       return widgetNode.getAttribute("removable") == "true";
  2359     // Otherwise this is either a special widget, which is always removable, or
  2360     // an API widget which has already been removed from gPalette. Returning true
  2361     // here allows us to then remove its ID from any placements where it might
  2362     // still occur.
  2363     return true;
  2364   },
  2366   canWidgetMoveToArea: function(aWidgetId, aArea) {
  2367     let placement = this.getPlacementOfWidget(aWidgetId);
  2368     if (placement && placement.area != aArea) {
  2369       // Special widgets can't move to the menu panel.
  2370       if (this.isSpecialWidget(aWidgetId) && gAreas.has(aArea) &&
  2371           gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL) {
  2372         return false;
  2374       // For everything else, just return whether the widget is removable.
  2375       return this.isWidgetRemovable(aWidgetId);
  2378     return true;
  2379   },
  2381   ensureWidgetPlacedInWindow: function(aWidgetId, aWindow) {
  2382     let placement = this.getPlacementOfWidget(aWidgetId);
  2383     if (!placement) {
  2384       return false;
  2386     let areaNodes = gBuildAreas.get(placement.area);
  2387     if (!areaNodes) {
  2388       return false;
  2390     let container = [...areaNodes].filter((n) => n.ownerDocument.defaultView == aWindow);
  2391     if (!container.length) {
  2392       return false;
  2394     let existingNode = container[0].getElementsByAttribute("id", aWidgetId)[0];
  2395     if (existingNode) {
  2396       return true;
  2399     this.insertNodeInWindow(aWidgetId, container[0], true);
  2400     return true;
  2401   },
  2403   get inDefaultState() {
  2404     for (let [areaId, props] of gAreas) {
  2405       let defaultPlacements = props.get("defaultPlacements");
  2406       // Areas without default placements (like legacy ones?) get skipped
  2407       if (!defaultPlacements) {
  2408         continue;
  2411       let currentPlacements = gPlacements.get(areaId);
  2412       // We're excluding all of the placement IDs for items that do not exist,
  2413       // and items that have removable="false",
  2414       // because we don't want to consider them when determining if we're
  2415       // in the default state. This way, if an add-on introduces a widget
  2416       // and is then uninstalled, the leftover placement doesn't cause us to
  2417       // automatically assume that the buttons are not in the default state.
  2418       let buildAreaNodes = gBuildAreas.get(areaId);
  2419       if (buildAreaNodes && buildAreaNodes.size) {
  2420         let container = [...buildAreaNodes][0];
  2421         let removableOrDefault = (itemNodeOrItem) => {
  2422           let item = (itemNodeOrItem && itemNodeOrItem.id) || itemNodeOrItem;
  2423           let isRemovable = this.isWidgetRemovable(itemNodeOrItem);
  2424           let isInDefault = defaultPlacements.indexOf(item) != -1;
  2425           return isRemovable || isInDefault;
  2426         };
  2427         // Toolbars have a currentSet property which also deals correctly with overflown
  2428         // widgets (if any) - use that instead:
  2429         if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
  2430           let currentSet = container.currentSet;
  2431           currentPlacements = currentSet ? currentSet.split(',') : [];
  2432           currentPlacements = currentPlacements.filter(removableOrDefault);
  2433         } else {
  2434           // Clone the array so we don't modify the actual placements...
  2435           currentPlacements = [...currentPlacements];
  2436           currentPlacements = currentPlacements.filter((item) => {
  2437             let itemNode = container.getElementsByAttribute("id", item)[0];
  2438             return itemNode && removableOrDefault(itemNode || item);
  2439           });
  2442         if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
  2443           let attribute = container.getAttribute("type") == "menubar" ? "autohide" : "collapsed";
  2444           let collapsed = container.getAttribute(attribute) == "true";
  2445           let defaultCollapsed = props.get("defaultCollapsed");
  2446           if (defaultCollapsed !== null && collapsed != defaultCollapsed) {
  2447             LOG("Found " + areaId + " had non-default toolbar visibility (expected " + defaultCollapsed + ", was " + collapsed + ")");
  2448             return false;
  2452       LOG("Checking default state for " + areaId + ":\n" + currentPlacements.join(",") +
  2453           "\nvs.\n" + defaultPlacements.join(","));
  2455       if (currentPlacements.length != defaultPlacements.length) {
  2456         return false;
  2459       for (let i = 0; i < currentPlacements.length; ++i) {
  2460         if (currentPlacements[i] != defaultPlacements[i]) {
  2461           LOG("Found " + currentPlacements[i] + " in " + areaId + " where " +
  2462               defaultPlacements[i] + " was expected!");
  2463           return false;
  2468     if (Services.prefs.prefHasUserValue(kPrefDrawInTitlebar)) {
  2469       LOG(kPrefDrawInTitlebar + " pref is non-default");
  2470       return false;
  2473     return true;
  2474   },
  2476   setToolbarVisibility: function(aToolbarId, aIsVisible) {
  2477     // We only persist the attribute the first time.
  2478     let isFirstChangedToolbar = true;
  2479     for (let window of CustomizableUI.windows) {
  2480       let toolbar = window.document.getElementById(aToolbarId);
  2481       if (toolbar) {
  2482         window.setToolbarVisibility(toolbar, aIsVisible, isFirstChangedToolbar);
  2483         isFirstChangedToolbar = false;
  2486   },
  2487 };
  2488 Object.freeze(CustomizableUIInternal);
  2490 this.CustomizableUI = {
  2491   /**
  2492    * Constant reference to the ID of the menu panel.
  2493    */
  2494   get AREA_PANEL() "PanelUI-contents",
  2495   /**
  2496    * Constant reference to the ID of the navigation toolbar.
  2497    */
  2498   get AREA_NAVBAR() "nav-bar",
  2499   /**
  2500    * Constant reference to the ID of the menubar's toolbar.
  2501    */
  2502   get AREA_MENUBAR() "toolbar-menubar",
  2503   /**
  2504    * Constant reference to the ID of the tabstrip toolbar.
  2505    */
  2506   get AREA_TABSTRIP() "TabsToolbar",
  2507   /**
  2508    * Constant reference to the ID of the bookmarks toolbar.
  2509    */
  2510   get AREA_BOOKMARKS() "PersonalToolbar",
  2511   /**
  2512    * Constant reference to the ID of the addon-bar toolbar shim.
  2513    * Do not use, this will be removed as soon as reasonably possible.
  2514    * @deprecated
  2515    */
  2516   get AREA_ADDONBAR() "addon-bar",
  2517   /**
  2518    * Constant indicating the area is a menu panel.
  2519    */
  2520   get TYPE_MENU_PANEL() "menu-panel",
  2521   /**
  2522    * Constant indicating the area is a toolbar.
  2523    */
  2524   get TYPE_TOOLBAR() "toolbar",
  2526   /**
  2527    * Constant indicating a XUL-type provider.
  2528    */
  2529   get PROVIDER_XUL() "xul",
  2530   /**
  2531    * Constant indicating an API-type provider.
  2532    */
  2533   get PROVIDER_API() "api",
  2534   /**
  2535    * Constant indicating dynamic (special) widgets: spring, spacer, and separator.
  2536    */
  2537   get PROVIDER_SPECIAL() "special",
  2539   /**
  2540    * Constant indicating the widget is built-in
  2541    */
  2542   get SOURCE_BUILTIN() "builtin",
  2543   /**
  2544    * Constant indicating the widget is externally provided
  2545    * (e.g. by add-ons or other items not part of the builtin widget set).
  2546    */
  2547   get SOURCE_EXTERNAL() "external",
  2549   /**
  2550    * The class used to distinguish items that span the entire menu panel.
  2551    */
  2552   get WIDE_PANEL_CLASS() "panel-wide-item",
  2553   /**
  2554    * The (constant) number of columns in the menu panel.
  2555    */
  2556   get PANEL_COLUMN_COUNT() 3,
  2558   /**
  2559    * Constant indicating the reason the event was fired was a window closing
  2560    */
  2561   get REASON_WINDOW_CLOSED() "window-closed",
  2562   /**
  2563    * Constant indicating the reason the event was fired was an area being
  2564    * unregistered separately from window closing mechanics.
  2565    */
  2566   get REASON_AREA_UNREGISTERED() "area-unregistered",
  2569   /**
  2570    * An iteratable property of windows managed by CustomizableUI.
  2571    * Note that this can *only* be used as an iterator. ie:
  2572    *     for (let window of CustomizableUI.windows) { ... }
  2573    */
  2574   windows: {
  2575     "@@iterator": function*() {
  2576       for (let [window,] of gBuildWindows)
  2577         yield window;
  2579   },
  2581   /**
  2582    * Add a listener object that will get fired for various events regarding
  2583    * customization.
  2585    * @param aListener the listener object to add
  2587    * Not all event handler methods need to be defined.
  2588    * CustomizableUI will catch exceptions. Events are dispatched
  2589    * synchronously on the UI thread, so if you can delay any/some of your
  2590    * processing, that is advisable. The following event handlers are supported:
  2591    *   - onWidgetAdded(aWidgetId, aArea, aPosition)
  2592    *     Fired when a widget is added to an area. aWidgetId is the widget that
  2593    *     was added, aArea the area it was added to, and aPosition the position
  2594    *     in which it was added.
  2595    *   - onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition)
  2596    *     Fired when a widget is moved within its area. aWidgetId is the widget
  2597    *     that was moved, aArea the area it was moved in, aOldPosition its old
  2598    *     position, and aNewPosition its new position.
  2599    *   - onWidgetRemoved(aWidgetId, aArea)
  2600    *     Fired when a widget is removed from its area. aWidgetId is the widget
  2601    *     that was removed, aArea the area it was removed from.
  2603    *   - onWidgetBeforeDOMChange(aNode, aNextNode, aContainer, aIsRemoval)
  2604    *     Fired *before* a widget's DOM node is acted upon by CustomizableUI
  2605    *     (to add, move or remove it). aNode is the DOM node changed, aNextNode
  2606    *     the DOM node (if any) before which a widget will be inserted,
  2607    *     aContainer the *actual* DOM container (could be an overflow panel in
  2608    *     case of an overflowable toolbar), and aWasRemoval is true iff the
  2609    *     action about to happen is the removal of the DOM node.
  2610    *   - onWidgetAfterDOMChange(aNode, aNextNode, aContainer, aWasRemoval)
  2611    *     Like onWidgetBeforeDOMChange, but fired after the change to the DOM
  2612    *     node of the widget.
  2614    *   - onWidgetReset(aNode, aContainer)
  2615    *     Fired after a reset to default placements moves a widget's node to a
  2616    *     different location. aNode is the widget's node, aContainer is the
  2617    *     area it was moved into (NB: it might already have been there and been
  2618    *     moved to a different position!)
  2619    *   - onWidgetUndoMove(aNode, aContainer)
  2620    *     Fired after undoing a reset to default placements moves a widget's
  2621    *     node to a different location. aNode is the widget's node, aContainer
  2622    *     is the area it was moved into (NB: it might already have been there
  2623    *     and been moved to a different position!)
  2624    *   - onAreaReset(aArea, aContainer)
  2625    *     Fired after a reset to default placements is complete on an area's
  2626    *     DOM node. Note that this is fired for each DOM node. aArea is the area
  2627    *     that was reset, aContainer the DOM node that was reset.
  2629    *   - onWidgetCreated(aWidgetId)
  2630    *     Fired when a widget with id aWidgetId has been created, but before it
  2631    *     is added to any placements or any DOM nodes have been constructed.
  2632    *     Only fired for API-based widgets.
  2633    *   - onWidgetAfterCreation(aWidgetId, aArea)
  2634    *     Fired after a widget with id aWidgetId has been created, and has been
  2635    *     added to either its default area or the area in which it was placed
  2636    *     previously. If the widget has no default area and/or it has never
  2637    *     been placed anywhere, aArea may be null. Only fired for API-based
  2638    *     widgets.
  2639    *   - onWidgetDestroyed(aWidgetId)
  2640    *     Fired when widgets are destroyed. aWidgetId is the widget that is
  2641    *     being destroyed. Only fired for API-based widgets.
  2642    *   - onWidgetInstanceRemoved(aWidgetId, aDocument)
  2643    *     Fired when a window is unloaded and a widget's instance is destroyed
  2644    *     because of this. Only fired for API-based widgets.
  2646    *   - onWidgetDrag(aWidgetId, aArea)
  2647    *     Fired both when and after customize mode drag handling system tries
  2648    *     to determine the width and height of widget aWidgetId when dragged to a
  2649    *     different area. aArea will be the area the item is dragged to, or
  2650    *     undefined after the measurements have been done and the node has been
  2651    *     moved back to its 'regular' area.
  2653    *   - onCustomizeStart(aWindow)
  2654    *     Fired when opening customize mode in aWindow.
  2655    *   - onCustomizeEnd(aWindow)
  2656    *     Fired when exiting customize mode in aWindow.
  2658    *   - onWidgetOverflow(aNode, aContainer)
  2659    *     Fired when a widget's DOM node is overflowing its container, a toolbar,
  2660    *     and will be displayed in the overflow panel.
  2661    *   - onWidgetUnderflow(aNode, aContainer)
  2662    *     Fired when a widget's DOM node is *not* overflowing its container, a
  2663    *     toolbar, anymore.
  2664    *   - onWindowOpened(aWindow)
  2665    *     Fired when a window has been opened that is managed by CustomizableUI,
  2666    *     once all of the prerequisite setup has been done.
  2667    *   - onWindowClosed(aWindow)
  2668    *     Fired when a window that has been managed by CustomizableUI has been
  2669    *     closed.
  2670    *   - onAreaNodeRegistered(aArea, aContainer)
  2671    *     Fired after an area node is first built when it is registered. This
  2672    *     is often when the window has opened, but in the case of add-ons,
  2673    *     could fire when the node has just been registered with CustomizableUI
  2674    *     after an add-on update or disable/enable sequence.
  2675    *   - onAreaNodeUnregistered(aArea, aContainer, aReason)
  2676    *     Fired when an area node is explicitly unregistered by an API caller,
  2677    *     or by a window closing. The aReason parameter indicates which of
  2678    *     these is the case.
  2679    */
  2680   addListener: function(aListener) {
  2681     CustomizableUIInternal.addListener(aListener);
  2682   },
  2683   /**
  2684    * Remove a listener added with addListener
  2685    * @param aListener the listener object to remove
  2686    */
  2687   removeListener: function(aListener) {
  2688     CustomizableUIInternal.removeListener(aListener);
  2689   },
  2691   /**
  2692    * Register a customizable area with CustomizableUI.
  2693    * @param aName   the name of the area to register. Can only contain
  2694    *                alphanumeric characters, dashes (-) and underscores (_).
  2695    * @param aProps  the properties of the area. The following properties are
  2696    *                recognized:
  2697    *                - type:   the type of area. Either TYPE_TOOLBAR (default) or
  2698    *                          TYPE_MENU_PANEL;
  2699    *                - anchor: for a menu panel or overflowable toolbar, the
  2700    *                          anchoring node for the panel.
  2701    *                - legacy: set to true if you want customizableui to
  2702    *                          automatically migrate the currentset attribute
  2703    *                - overflowable: set to true if your toolbar is overflowable.
  2704    *                                This requires an anchor, and only has an
  2705    *                                effect for toolbars.
  2706    *                - defaultPlacements: an array of widget IDs making up the
  2707    *                                     default contents of the area
  2708    *                - defaultCollapsed: (INTERNAL ONLY) applies if the type is TYPE_TOOLBAR, specifies
  2709    *                                    if toolbar is collapsed by default (default to true).
  2710    *                                    Specify null to ensure that reset/inDefaultArea don't care
  2711    *                                    about a toolbar's collapsed state
  2712    */
  2713   registerArea: function(aName, aProperties) {
  2714     CustomizableUIInternal.registerArea(aName, aProperties);
  2715   },
  2716   /**
  2717    * Register a concrete node for a registered area. This method is automatically
  2718    * called from any toolbar in the main browser window that has its
  2719    * "customizable" attribute set to true. There should normally be no need to
  2720    * call it yourself.
  2722    * Note that ideally, you should register your toolbar using registerArea
  2723    * before any of the toolbars have their XBL bindings constructed (which
  2724    * will happen when they're added to the DOM and are not hidden). If you
  2725    * don't, and your toolbar has a defaultset attribute, CustomizableUI will
  2726    * register it automatically. If your toolbar does not have a defaultset
  2727    * attribute, the node will be saved for processing when you call
  2728    * registerArea. Note that CustomizableUI won't restore state in the area,
  2729    * allow the user to customize it in customize mode, or otherwise deal
  2730    * with it, until the area has been registered.
  2731    */
  2732   registerToolbarNode: function(aToolbar, aExistingChildren) {
  2733     CustomizableUIInternal.registerToolbarNode(aToolbar, aExistingChildren);
  2734   },
  2735   /**
  2736    * Register the menu panel node. This method should not be called by anyone
  2737    * apart from the built-in PanelUI.
  2738    * @param aPanel the panel DOM node being registered.
  2739    */
  2740   registerMenuPanel: function(aPanel) {
  2741     CustomizableUIInternal.registerMenuPanel(aPanel);
  2742   },
  2743   /**
  2744    * Unregister a customizable area. The inverse of registerArea.
  2746    * Unregistering an area will remove all the (removable) widgets in the
  2747    * area, which will return to the panel, and destroy all other traces
  2748    * of the area within CustomizableUI. Note that this means the *contents*
  2749    * of the area's DOM nodes will be moved to the panel or removed, but
  2750    * the area's DOM nodes *themselves* will stay.
  2752    * Furthermore, by default the placements of the area will be kept in the
  2753    * saved state (!) and restored if you re-register the area at a later
  2754    * point. This is useful for e.g. add-ons that get disabled and then
  2755    * re-enabled (e.g. when they update).
  2757    * You can override this last behaviour (and destroy the placements
  2758    * information in the saved state) by passing true for aDestroyPlacements.
  2760    * @param aName              the name of the area to unregister
  2761    * @param aDestroyPlacements whether to destroy the placements information
  2762    *                           for the area, too.
  2763    */
  2764   unregisterArea: function(aName, aDestroyPlacements) {
  2765     CustomizableUIInternal.unregisterArea(aName, aDestroyPlacements);
  2766   },
  2767   /**
  2768    * Add a widget to an area.
  2769    * If the area to which you try to add is not known to CustomizableUI,
  2770    * this will throw.
  2771    * If the area to which you try to add has not yet been restored from its
  2772    * legacy state, this will postpone the addition.
  2773    * If the area to which you try to add is the same as the area in which
  2774    * the widget is currently placed, this will do the same as
  2775    * moveWidgetWithinArea.
  2776    * If the widget cannot be removed from its original location, this will
  2777    * no-op.
  2779    * This will fire an onWidgetAdded notification,
  2780    * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification
  2781    * for each window CustomizableUI knows about.
  2783    * @param aWidgetId the ID of the widget to add
  2784    * @param aArea     the ID of the area to add the widget to
  2785    * @param aPosition the position at which to add the widget. If you do not
  2786    *                  pass a position, the widget will be added to the end
  2787    *                  of the area.
  2788    */
  2789   addWidgetToArea: function(aWidgetId, aArea, aPosition) {
  2790     CustomizableUIInternal.addWidgetToArea(aWidgetId, aArea, aPosition);
  2791   },
  2792   /**
  2793    * Remove a widget from its area. If the widget cannot be removed from its
  2794    * area, or is not in any area, this will no-op. Otherwise, this will fire an
  2795    * onWidgetRemoved notification, and an onWidgetBeforeDOMChange and
  2796    * onWidgetAfterDOMChange notification for each window CustomizableUI knows
  2797    * about.
  2799    * @param aWidgetId the ID of the widget to remove
  2800    */
  2801   removeWidgetFromArea: function(aWidgetId) {
  2802     CustomizableUIInternal.removeWidgetFromArea(aWidgetId);
  2803   },
  2804   /**
  2805    * Move a widget within an area.
  2806    * If the widget is not in any area, this will no-op.
  2807    * If the widget is already at the indicated position, this will no-op.
  2809    * Otherwise, this will move the widget and fire an onWidgetMoved notification,
  2810    * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification for
  2811    * each window CustomizableUI knows about.
  2813    * @param aWidgetId the ID of the widget to move
  2814    * @param aPosition the position to move the widget to.
  2815    *                  Negative values or values greater than the number of
  2816    *                  widgets will be interpreted to mean moving the widget to
  2817    *                  respectively the first or last position.
  2818    */
  2819   moveWidgetWithinArea: function(aWidgetId, aPosition) {
  2820     CustomizableUIInternal.moveWidgetWithinArea(aWidgetId, aPosition);
  2821   },
  2822   /**
  2823    * Ensure a XUL-based widget created in a window after areas were
  2824    * initialized moves to its correct position.
  2825    * This is roughly equivalent to manually looking up the position and using
  2826    * insertItem in the old API, but a lot less work for consumers.
  2827    * Always prefer this over using toolbar.insertItem (which might no-op
  2828    * because it delegates to addWidgetToArea) or, worse, moving items in the
  2829    * DOM yourself.
  2831    * @param aWidgetId the ID of the widget that was just created
  2832    * @param aWindow the window in which you want to ensure it was added.
  2834    * NB: why is this API per-window, you wonder? Because if you need this,
  2835    * presumably you yourself need to create the widget in all the windows
  2836    * and need to loop through them anyway.
  2837    */
  2838   ensureWidgetPlacedInWindow: function(aWidgetId, aWindow) {
  2839     return CustomizableUIInternal.ensureWidgetPlacedInWindow(aWidgetId, aWindow);
  2840   },
  2841   /**
  2842    * Start a batch update of items.
  2843    * During a batch update, the customization state is not saved to the user's
  2844    * preferences file, in order to reduce (possibly sync) IO.
  2845    * Calls to begin/endBatchUpdate may be nested.
  2847    * Callers should ensure that NO MATTER WHAT they call endBatchUpdate once
  2848    * for each call to beginBatchUpdate, even if there are exceptions in the
  2849    * code in the batch update. Otherwise, for the duration of the
  2850    * Firefox session, customization state is never saved. Typically, you
  2851    * would do this using a try...finally block.
  2852    */
  2853   beginBatchUpdate: function() {
  2854     CustomizableUIInternal.beginBatchUpdate();
  2855   },
  2856   /**
  2857    * End a batch update. See the documentation for beginBatchUpdate above.
  2859    * State is not saved if we believe it is identical to the last known
  2860    * saved state. State is only ever saved when all batch updates have
  2861    * finished (ie there has been 1 endBatchUpdate call for each
  2862    * beginBatchUpdate call). If any of the endBatchUpdate calls pass
  2863    * aForceDirty=true, we will flush to the prefs file.
  2865    * @param aForceDirty force CustomizableUI to flush to the prefs file when
  2866    *                    all batch updates have finished.
  2867    */
  2868   endBatchUpdate: function(aForceDirty) {
  2869     CustomizableUIInternal.endBatchUpdate(aForceDirty);
  2870   },
  2871   /**
  2872    * Create a widget.
  2874    * To create a widget, you should pass an object with its desired
  2875    * properties. The following properties are supported:
  2877    * - id:            the ID of the widget (required).
  2878    * - type:          a string indicating the type of widget. Possible types
  2879    *                  are:
  2880    *                  'button' - for simple button widgets (the default)
  2881    *                  'view'   - for buttons that open a panel or subview,
  2882    *                             depending on where they are placed.
  2883    *                  'custom' - for fine-grained control over the creation
  2884    *                             of the widget.
  2885    * - viewId:        Only useful for views (and required there): the id of the
  2886    *                  <panelview> that should be shown when clicking the widget.
  2887    * - onBuild(aDoc): Only useful for custom widgets (and required there); a
  2888    *                  function that will be invoked with the document in which
  2889    *                  to build a widget. Should return the DOM node that has
  2890    *                  been constructed.
  2891    * - onBeforeCreated(aDoc): Attached to all non-custom widgets; a function
  2892    *                  that will be invoked before the widget gets a DOM node
  2893    *                  constructed, passing the document in which that will happen.
  2894    *                  This is useful especially for 'view' type widgets that need
  2895    *                  to construct their views on the fly (e.g. from bootstrapped
  2896    *                  add-ons)
  2897    * - onCreated(aNode): Attached to all widgets; a function that will be invoked
  2898    *                  whenever the widget has a DOM node constructed, passing the
  2899    *                  constructed node as an argument.
  2900    * - onCommand(aEvt): Only useful for button widgets; a function that will be
  2901    *                    invoked when the user activates the button.
  2902    * - onClick(aEvt): Attached to all widgets; a function that will be invoked
  2903    *                  when the user clicks the widget.
  2904    * - onViewShowing(aEvt): Only useful for views; a function that will be
  2905    *                  invoked when a user shows your view.
  2906    * - onViewHiding(aEvt): Only useful for views; a function that will be
  2907    *                  invoked when a user hides your view.
  2908    * - tooltiptext:   string to use for the tooltip of the widget
  2909    * - label:         string to use for the label of the widget
  2910    * - removable:     whether the widget is removable (optional, default: true)
  2911    *                  NB: if you specify false here, you must provide a
  2912    *                  defaultArea, too.
  2913    * - overflows:     whether widget can overflow when in an overflowable
  2914    *                  toolbar (optional, default: true)
  2915    * - defaultArea:   default area to add the widget to
  2916    *                  (optional, default: none; required if non-removable)
  2917    * - shortcutId:    id of an element that has a shortcut for this widget
  2918    *                  (optional, default: null). This is only used to display
  2919    *                  the shortcut as part of the tooltip for builtin widgets
  2920    *                  (which have strings inside
  2921    *                  customizableWidgets.properties). If you're in an add-on,
  2922    *                  you should not set this property.
  2923    * - showInPrivateBrowsing: whether to show the widget in private browsing
  2924    *                          mode (optional, default: true)
  2926    * @param aProperties the specifications for the widget.
  2927    * @return a wrapper around the created widget (see getWidget)
  2928    */
  2929   createWidget: function(aProperties) {
  2930     return CustomizableUIInternal.wrapWidget(
  2931       CustomizableUIInternal.createWidget(aProperties)
  2932     );
  2933   },
  2934   /**
  2935    * Destroy a widget
  2937    * If the widget is part of the default placements in an area, this will
  2938    * remove it from there. It will also remove any DOM instances. However,
  2939    * it will keep the widget in the placements for whatever area it was
  2940    * in at the time. You can remove it from there yourself by calling
  2941    * CustomizableUI.removeWidgetFromArea(aWidgetId).
  2943    * @param aWidgetId the ID of the widget to destroy
  2944    */
  2945   destroyWidget: function(aWidgetId) {
  2946     CustomizableUIInternal.destroyWidget(aWidgetId);
  2947   },
  2948   /**
  2949    * Get a wrapper object with information about the widget.
  2950    * The object provides the following properties
  2951    * (all read-only unless otherwise indicated):
  2953    * - id:            the widget's ID;
  2954    * - type:          the type of widget (button, view, custom). For
  2955    *                  XUL-provided widgets, this is always 'custom';
  2956    * - provider:      the provider type of the widget, id est one of
  2957    *                  PROVIDER_API or PROVIDER_XUL;
  2958    * - forWindow(w):  a method to obtain a single window wrapper for a widget,
  2959    *                  in the window w passed as the only argument;
  2960    * - instances:     an array of all instances (single window wrappers)
  2961    *                  of the widget. This array is NOT live;
  2962    * - areaType:      the type of the widget's current area
  2963    * - isGroup:       true; will be false for wrappers around single widget nodes;
  2964    * - source:        for API-provided widgets, whether they are built-in to
  2965    *                  Firefox or add-on-provided;
  2966    * - disabled:      for API-provided widgets, whether the widget is currently
  2967    *                  disabled. NB: this property is writable, and will toggle
  2968    *                  all the widgets' nodes' disabled states;
  2969    * - label:         for API-provied widgets, the label of the widget;
  2970    * - tooltiptext:   for API-provided widgets, the tooltip of the widget;
  2971    * - showInPrivateBrowsing: for API-provided widgets, whether the widget is
  2972    *                          visible in private browsing;
  2974    * Single window wrappers obtained through forWindow(someWindow) or from the
  2975    * instances array have the following properties
  2976    * (all read-only unless otherwise indicated):
  2978    * - id:            the widget's ID;
  2979    * - type:          the type of widget (button, view, custom). For
  2980    *                  XUL-provided widgets, this is always 'custom';
  2981    * - provider:      the provider type of the widget, id est one of
  2982    *                  PROVIDER_API or PROVIDER_XUL;
  2983    * - node:          reference to the corresponding DOM node;
  2984    * - anchor:        the anchor on which to anchor panels opened from this
  2985    *                  node. This will point to the overflow chevron on
  2986    *                  overflowable toolbars if and only if your widget node
  2987    *                  is overflowed, to the anchor for the panel menu
  2988    *                  if your widget is inside the panel menu, and to the
  2989    *                  node itself in all other cases;
  2990    * - overflowed:    boolean indicating whether the node is currently in the
  2991    *                  overflow panel of the toolbar;
  2992    * - isGroup:       false; will be true for the group widget;
  2993    * - label:         for API-provided widgets, convenience getter for the
  2994    *                  label attribute of the DOM node;
  2995    * - tooltiptext:   for API-provided widgets, convenience getter for the
  2996    *                  tooltiptext attribute of the DOM node;
  2997    * - disabled:      for API-provided widgets, convenience getter *and setter*
  2998    *                  for the disabled state of this single widget. Note that
  2999    *                  you may prefer to use the group wrapper's getter/setter
  3000    *                  instead.
  3002    * @param aWidgetId the ID of the widget whose information you need
  3003    * @return a wrapper around the widget as described above, or null if the
  3004    *         widget is known not to exist (anymore). NB: non-null return
  3005    *         is no guarantee the widget exists because we cannot know in
  3006    *         advance if a XUL widget exists or not.
  3007    */
  3008   getWidget: function(aWidgetId) {
  3009     return CustomizableUIInternal.wrapWidget(aWidgetId);
  3010   },
  3011   /**
  3012    * Get an array of widget wrappers (see getWidget) for all the widgets
  3013    * which are currently not in any area (so which are in the palette).
  3015    * @param aWindowPalette the palette (and by extension, the window) in which
  3016    *                       CustomizableUI should look. This matters because of
  3017    *                       course XUL-provided widgets could be available in
  3018    *                       some windows but not others, and likewise
  3019    *                       API-provided widgets might not exist in a private
  3020    *                       window (because of the showInPrivateBrowsing
  3021    *                       property).
  3023    * @return an array of widget wrappers (see getWidget)
  3024    */
  3025   getUnusedWidgets: function(aWindowPalette) {
  3026     return CustomizableUIInternal.getUnusedWidgets(aWindowPalette).map(
  3027       CustomizableUIInternal.wrapWidget,
  3028       CustomizableUIInternal
  3029     );
  3030   },
  3031   /**
  3032    * Get an array of all the widget IDs placed in an area. This is roughly
  3033    * equivalent to fetching the currentset attribute and splitting by commas
  3034    * in the legacy APIs. Modifying the array will not affect CustomizableUI.
  3036    * @param aArea the ID of the area whose placements you want to obtain.
  3037    * @return an array containing the widget IDs that are in the area.
  3039    * NB: will throw if called too early (before placements have been fetched)
  3040    *     or if the area is not currently known to CustomizableUI.
  3041    */
  3042   getWidgetIdsInArea: function(aArea) {
  3043     if (!gAreas.has(aArea)) {
  3044       throw new Error("Unknown customization area: " + aArea);
  3046     if (!gPlacements.has(aArea)) {
  3047       throw new Error("Area not yet restored");
  3050     // We need to clone this, as we don't want to let consumers muck with placements
  3051     return [...gPlacements.get(aArea)];
  3052   },
  3053   /**
  3054    * Get an array of widget wrappers for all the widgets in an area. This is
  3055    * the same as calling getWidgetIdsInArea and .map() ing the result through
  3056    * CustomizableUI.getWidget. Careful: this means that if there are IDs in there
  3057    * which don't have corresponding DOM nodes (like in the old-style currentset
  3058    * attribute), there might be nulls in this array, or items for which
  3059    * wrapper.forWindow(win) will return null.
  3061    * @param aArea the ID of the area whose widgets you want to obtain.
  3062    * @return an array of widget wrappers and/or null values for the widget IDs
  3063    *         placed in an area.
  3065    * NB: will throw if called too early (before placements have been fetched)
  3066    *     or if the area is not currently known to CustomizableUI.
  3067    */
  3068   getWidgetsInArea: function(aArea) {
  3069     return this.getWidgetIdsInArea(aArea).map(
  3070       CustomizableUIInternal.wrapWidget,
  3071       CustomizableUIInternal
  3072     );
  3073   },
  3074   /**
  3075    * Obtain an array of all the area IDs known to CustomizableUI.
  3076    * This array is created for you, so is modifiable without CustomizableUI
  3077    * being affected.
  3078    */
  3079   get areas() {
  3080     return [area for ([area, props] of gAreas)];
  3081   },
  3082   /**
  3083    * Check what kind of area (toolbar or menu panel) an area is. This is
  3084    * useful if you have a widget that needs to behave differently depending
  3085    * on its location. Note that widget wrappers have a convenience getter
  3086    * property (areaType) for this purpose.
  3088    * @param aArea the ID of the area whose type you want to know
  3089    * @return TYPE_TOOLBAR or TYPE_MENU_PANEL depending on the area, null if
  3090    *         the area is unknown.
  3091    */
  3092   getAreaType: function(aArea) {
  3093     let area = gAreas.get(aArea);
  3094     return area ? area.get("type") : null;
  3095   },
  3096   /**
  3097    * Check if a toolbar is collapsed by default.
  3099    * @param aArea the ID of the area whose default-collapsed state you want to know.
  3100    * @return `true` or `false` depending on the area, null if the area is unknown,
  3101    *         or its collapsed state cannot normally be controlled by the user
  3102    */
  3103   isToolbarDefaultCollapsed: function(aArea) {
  3104     let area = gAreas.get(aArea);
  3105     return area ? area.get("defaultCollapsed") : null;
  3106   },
  3107   /**
  3108    * Obtain the DOM node that is the customize target for an area in a
  3109    * specific window.
  3111    * Areas can have a customization target that does not correspond to the
  3112    * node itself. In particular, toolbars that have a customizationtarget
  3113    * attribute set will have their customization target set to that node.
  3114    * This means widgets will end up in the customization target, not in the
  3115    * DOM node with the ID that corresponds to the area ID. This is useful
  3116    * because it lets you have fixed content in a toolbar (e.g. the panel
  3117    * menu item in the navbar) and have all the customizable widgets use
  3118    * the customization target.
  3120    * Using this API yourself is discouraged; you should generally not need
  3121    * to be asking for the DOM container node used for a particular area.
  3122    * In particular, if you're wanting to check it in relation to a widget's
  3123    * node, your DOM node might not be a direct child of the customize target
  3124    * in a window if, for instance, the window is in customization mode, or if
  3125    * this is an overflowable toolbar and the widget has been overflowed.
  3127    * @param aArea   the ID of the area whose customize target you want to have
  3128    * @param aWindow the window where you want to fetch the DOM node.
  3129    * @return the customize target DOM node for aArea in aWindow
  3130    */
  3131   getCustomizeTargetForArea: function(aArea, aWindow) {
  3132     return CustomizableUIInternal.getCustomizeTargetForArea(aArea, aWindow);
  3133   },
  3134   /**
  3135    * Reset the customization state back to its default.
  3137    * This is the nuclear option. You should never call this except if the user
  3138    * explicitly requests it. Firefox does this when the user clicks the
  3139    * "Restore Defaults" button in customize mode.
  3140    */
  3141   reset: function() {
  3142     CustomizableUIInternal.reset();
  3143   },
  3145   /**
  3146    * Undo the previous reset, can only be called immediately after a reset.
  3147    * @return a promise that will be resolved when the operation is complete.
  3148    */
  3149   undoReset: function() {
  3150     CustomizableUIInternal.undoReset();
  3151   },
  3153   /**
  3154    * Remove a custom toolbar added in a previous version of Firefox or using
  3155    * an add-on. NB: only works on the customizable toolbars generated by
  3156    * the toolbox itself. Intended for use from CustomizeMode, not by
  3157    * other consumers.
  3158    * @param aToolbarId the ID of the toolbar to remove
  3159    */
  3160   removeExtraToolbar: function(aToolbarId) {
  3161     CustomizableUIInternal.removeExtraToolbar(aToolbarId);
  3162   },
  3164   /**
  3165    * Can the last Restore Defaults operation be undone.
  3167    * @return A boolean stating whether an undo of the
  3168    *         Restore Defaults can be performed.
  3169    */
  3170   get canUndoReset() {
  3171     return gUIStateBeforeReset.uiCustomizationState != null ||
  3172            gUIStateBeforeReset.drawInTitlebar != null;
  3173   },
  3175   /**
  3176    * Get the placement of a widget. This is by far the best way to obtain
  3177    * information about what the state of your widget is. The internals of
  3178    * this call are cheap (no DOM necessary) and you will know where the user
  3179    * has put your widget.
  3181    * @param aWidgetId the ID of the widget whose placement you want to know
  3182    * @return
  3183    *   {
  3184    *     area: "somearea", // The ID of the area where the widget is placed
  3185    *     position: 42 // the index in the placements array corresponding to
  3186    *                  // your widget.
  3187    *   }
  3189    *   OR
  3191    *   null // if the widget is not placed anywhere (ie in the palette)
  3192    */
  3193   getPlacementOfWidget: function(aWidgetId) {
  3194     return CustomizableUIInternal.getPlacementOfWidget(aWidgetId, true);
  3195   },
  3196   /**
  3197    * Check if a widget can be removed from the area it's in.
  3199    * Note that if you're wanting to move the widget somewhere, you should
  3200    * generally be checking canWidgetMoveToArea, because that will return
  3201    * true if the widget is already in the area where you want to move it (!).
  3203    * NB: oh, also, this method might lie if the widget in question is a
  3204    *     XUL-provided widget and there are no windows open, because it
  3205    *     can obviously not check anything in this case. It will return
  3206    *     true. You will be able to move the widget elsewhere. However,
  3207    *     once the user reopens a window, the widget will move back to its
  3208    *     'proper' area automagically.
  3210    * @param aWidgetId a widget ID or DOM node to check
  3211    * @return true if the widget can be removed from its area,
  3212    *          false otherwise.
  3213    */
  3214   isWidgetRemovable: function(aWidgetId) {
  3215     return CustomizableUIInternal.isWidgetRemovable(aWidgetId);
  3216   },
  3217   /**
  3218    * Check if a widget can be moved to a particular area. Like
  3219    * isWidgetRemovable but better, because it'll return true if the widget
  3220    * is already in the right area.
  3222    * @param aWidgetId the widget ID or DOM node you want to move somewhere
  3223    * @param aArea     the area ID you want to move it to.
  3224    * @return true if this is possible, false if it is not. The same caveats as
  3225    *              for isWidgetRemovable apply, however, if no windows are open.
  3226    */
  3227   canWidgetMoveToArea: function(aWidgetId, aArea) {
  3228     return CustomizableUIInternal.canWidgetMoveToArea(aWidgetId, aArea);
  3229   },
  3230   /**
  3231    * Whether we're in a default state. Note that non-removable non-default
  3232    * widgets and non-existing widgets are not taken into account in determining
  3233    * whether we're in the default state.
  3235    * NB: this is a property with a getter. The getter is NOT cheap, because
  3236    * it does smart things with non-removable non-default items, non-existent
  3237    * items, and so forth. Please don't call unless necessary.
  3238    */
  3239   get inDefaultState() {
  3240     return CustomizableUIInternal.inDefaultState;
  3241   },
  3243   /**
  3244    * Set a toolbar's visibility state in all windows.
  3245    * @param aToolbarId    the toolbar whose visibility should be adjusted
  3246    * @param aIsVisible    whether the toolbar should be visible
  3247    */
  3248   setToolbarVisibility: function(aToolbarId, aIsVisible) {
  3249     CustomizableUIInternal.setToolbarVisibility(aToolbarId, aIsVisible);
  3250   },
  3252   /**
  3253    * Get a localized property off a (widget?) object.
  3255    * NB: this is unlikely to be useful unless you're in Firefox code, because
  3256    *     this code uses the builtin widget stringbundle, and can't be told
  3257    *     to use add-on-provided strings. It's mainly here as convenience for
  3258    *     custom builtin widgets that build their own DOM but use the same
  3259    *     stringbundle as the other builtin widgets.
  3261    * @param aWidget     the object whose property we should use to fetch a
  3262    *                    localizable string;
  3263    * @param aProp       the property on the object to use for the fetching;
  3264    * @param aFormatArgs (optional) any extra arguments to use for a formatted
  3265    *                    string;
  3266    * @param aDef        (optional) the default to return if we don't find the
  3267    *                    string in the stringbundle;
  3269    * @return the localized string, or aDef if the string isn't in the bundle.
  3270    *         If no default is provided,
  3271    *           if aProp exists on aWidget, we'll return that,
  3272    *           otherwise we'll return the empty string
  3274    */
  3275   getLocalizedProperty: function(aWidget, aProp, aFormatArgs, aDef) {
  3276     return CustomizableUIInternal.getLocalizedProperty(aWidget, aProp,
  3277       aFormatArgs, aDef);
  3278   },
  3279   /**
  3280    * Given a node, walk up to the first panel in its ancestor chain, and
  3281    * close it.
  3283    * @param aNode a node whose panel should be closed;
  3284    */
  3285   hidePanelForNode: function(aNode) {
  3286     CustomizableUIInternal.hidePanelForNode(aNode);
  3287   },
  3288   /**
  3289    * Check if a widget is a "special" widget: a spring, spacer or separator.
  3291    * @param aWidgetId the widget ID to check.
  3292    * @return true if the widget is 'special', false otherwise.
  3293    */
  3294   isSpecialWidget: function(aWidgetId) {
  3295     return CustomizableUIInternal.isSpecialWidget(aWidgetId);
  3296   },
  3297   /**
  3298    * Add listeners to a panel that will close it. For use from the menu panel
  3299    * and overflowable toolbar implementations, unlikely to be useful for
  3300    * consumers.
  3302    * @param aPanel the panel to which listeners should be attached.
  3303    */
  3304   addPanelCloseListeners: function(aPanel) {
  3305     CustomizableUIInternal.addPanelCloseListeners(aPanel);
  3306   },
  3307   /**
  3308    * Remove close listeners that have been added to a panel with
  3309    * addPanelCloseListeners. For use from the menu panel and overflowable
  3310    * toolbar implementations, unlikely to be useful for consumers.
  3312    * @param aPanel the panel from which listeners should be removed.
  3313    */
  3314   removePanelCloseListeners: function(aPanel) {
  3315     CustomizableUIInternal.removePanelCloseListeners(aPanel);
  3316   },
  3317   /**
  3318    * Notify listeners a widget is about to be dragged to an area. For use from
  3319    * Customize Mode only, do not use otherwise.
  3321    * @param aWidgetId the ID of the widget that is being dragged to an area.
  3322    * @param aArea     the ID of the area to which the widget is being dragged.
  3323    */
  3324   onWidgetDrag: function(aWidgetId, aArea) {
  3325     CustomizableUIInternal.notifyListeners("onWidgetDrag", aWidgetId, aArea);
  3326   },
  3327   /**
  3328    * Notify listeners that a window is entering customize mode. For use from
  3329    * Customize Mode only, do not use otherwise.
  3330    * @param aWindow the window entering customize mode
  3331    */
  3332   notifyStartCustomizing: function(aWindow) {
  3333     CustomizableUIInternal.notifyListeners("onCustomizeStart", aWindow);
  3334   },
  3335   /**
  3336    * Notify listeners that a window is exiting customize mode. For use from
  3337    * Customize Mode only, do not use otherwise.
  3338    * @param aWindow the window exiting customize mode
  3339    */
  3340   notifyEndCustomizing: function(aWindow) {
  3341     CustomizableUIInternal.notifyListeners("onCustomizeEnd", aWindow);
  3342   },
  3344   /**
  3345    * Notify toolbox(es) of a particular event. If you don't pass aWindow,
  3346    * all toolboxes will be notified. For use from Customize Mode only,
  3347    * do not use otherwise.
  3348    * @param aEvent the name of the event to send.
  3349    * @param aDetails optional, the details of the event.
  3350    * @param aWindow optional, the window in which to send the event.
  3351    */
  3352   dispatchToolboxEvent: function(aEvent, aDetails={}, aWindow=null) {
  3353     CustomizableUIInternal.dispatchToolboxEvent(aEvent, aDetails, aWindow);
  3354   },
  3356   /**
  3357    * Check whether an area is overflowable.
  3359    * @param aAreaId the ID of an area to check for overflowable-ness
  3360    * @return true if the area is overflowable, false otherwise.
  3361    */
  3362   isAreaOverflowable: function(aAreaId) {
  3363     let area = gAreas.get(aAreaId);
  3364     return area ? area.get("type") == this.TYPE_TOOLBAR && area.get("overflowable")
  3365                 : false;
  3366   },
  3367   /**
  3368    * Obtain a string indicating the place of an element. This is intended
  3369    * for use from customize mode; You should generally use getPlacementOfWidget
  3370    * instead, which is cheaper because it does not use the DOM.
  3372    * @param aElement the DOM node whose place we need to check
  3373    * @return "toolbar" if the node is in a toolbar, "panel" if it is in the
  3374    *         menu panel, "palette" if it is in the (visible!) customization
  3375    *         palette, undefined otherwise.
  3376    */
  3377   getPlaceForItem: function(aElement) {
  3378     let place;
  3379     let node = aElement;
  3380     while (node && !place) {
  3381       if (node.localName == "toolbar")
  3382         place = "toolbar";
  3383       else if (node.id == CustomizableUI.AREA_PANEL)
  3384         place = "panel";
  3385       else if (node.id == "customization-palette")
  3386         place = "palette";
  3388       node = node.parentNode;
  3390     return place;
  3391   },
  3393   /**
  3394    * Check if a toolbar is builtin or not.
  3395    * @param aToolbarId the ID of the toolbar you want to check
  3396    */
  3397   isBuiltinToolbar: function(aToolbarId) {
  3398     return CustomizableUIInternal._builtinToolbars.has(aToolbarId);
  3399   },
  3400 };
  3401 Object.freeze(this.CustomizableUI);
  3402 Object.freeze(this.CustomizableUI.windows);
  3404 /**
  3405  * All external consumers of widgets are really interacting with these wrappers
  3406  * which provide a common interface.
  3407  */
  3409 /**
  3410  * WidgetGroupWrapper is the common interface for interacting with an entire
  3411  * widget group - AKA, all instances of a widget across a series of windows.
  3412  * This particular wrapper is only used for widgets created via the provider
  3413  * API.
  3414  */
  3415 function WidgetGroupWrapper(aWidget) {
  3416   this.isGroup = true;
  3418   const kBareProps = ["id", "source", "type", "disabled", "label", "tooltiptext",
  3419                       "showInPrivateBrowsing"];
  3420   for (let prop of kBareProps) {
  3421     let propertyName = prop;
  3422     this.__defineGetter__(propertyName, function() aWidget[propertyName]);
  3425   this.__defineGetter__("provider", function() CustomizableUI.PROVIDER_API);
  3427   this.__defineSetter__("disabled", function(aValue) {
  3428     aValue = !!aValue;
  3429     aWidget.disabled = aValue;
  3430     for (let [,instance] of aWidget.instances) {
  3431       instance.disabled = aValue;
  3433   });
  3435   this.forWindow = function WidgetGroupWrapper_forWindow(aWindow) {
  3436     let wrapperMap;
  3437     if (!gSingleWrapperCache.has(aWindow)) {
  3438       wrapperMap = new Map();
  3439       gSingleWrapperCache.set(aWindow, wrapperMap);
  3440     } else {
  3441       wrapperMap = gSingleWrapperCache.get(aWindow);
  3443     if (wrapperMap.has(aWidget.id)) {
  3444       return wrapperMap.get(aWidget.id);
  3447     let instance = aWidget.instances.get(aWindow.document);
  3448     if (!instance &&
  3449         (aWidget.showInPrivateBrowsing || !PrivateBrowsingUtils.isWindowPrivate(aWindow))) {
  3450       instance = CustomizableUIInternal.buildWidget(aWindow.document,
  3451                                                     aWidget);
  3454     let wrapper = new WidgetSingleWrapper(aWidget, instance);
  3455     wrapperMap.set(aWidget.id, wrapper);
  3456     return wrapper;
  3457   };
  3459   this.__defineGetter__("instances", function() {
  3460     // Can't use gBuildWindows here because some areas load lazily:
  3461     let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id);
  3462     if (!placement) {
  3463       return [];
  3465     let area = placement.area;
  3466     let buildAreas = gBuildAreas.get(area);
  3467     if (!buildAreas) {
  3468       return [];
  3470     return [this.forWindow(node.ownerDocument.defaultView) for (node of buildAreas)];
  3471   });
  3473   this.__defineGetter__("areaType", function() {
  3474     let areaProps = gAreas.get(aWidget.currentArea);
  3475     return areaProps && areaProps.get("type");
  3476   });
  3478   Object.freeze(this);
  3481 /**
  3482  * A WidgetSingleWrapper is a wrapper around a single instance of a widget in
  3483  * a particular window.
  3484  */
  3485 function WidgetSingleWrapper(aWidget, aNode) {
  3486   this.isGroup = false;
  3488   this.node = aNode;
  3489   this.provider = CustomizableUI.PROVIDER_API;
  3491   const kGlobalProps = ["id", "type"];
  3492   for (let prop of kGlobalProps) {
  3493     this[prop] = aWidget[prop];
  3496   const kNodeProps = ["label", "tooltiptext"];
  3497   for (let prop of kNodeProps) {
  3498     let propertyName = prop;
  3499     // Look at the node for these, instead of the widget data, to ensure the
  3500     // wrapper always reflects this live instance.
  3501     this.__defineGetter__(propertyName,
  3502                           function() aNode.getAttribute(propertyName));
  3505   this.__defineGetter__("disabled", function() aNode.disabled);
  3506   this.__defineSetter__("disabled", function(aValue) {
  3507     aNode.disabled = !!aValue;
  3508   });
  3510   this.__defineGetter__("anchor", function() {
  3511     let anchorId;
  3512     // First check for an anchor for the area:
  3513     let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id);
  3514     if (placement) {
  3515       anchorId = gAreas.get(placement.area).get("anchor");
  3517     if (!anchorId) {
  3518       anchorId = aNode.getAttribute("cui-anchorid");
  3521     return anchorId ? aNode.ownerDocument.getElementById(anchorId)
  3522                     : aNode;
  3523   });
  3525   this.__defineGetter__("overflowed", function() {
  3526     return aNode.getAttribute("overflowedItem") == "true";
  3527   });
  3529   Object.freeze(this);
  3532 /**
  3533  * XULWidgetGroupWrapper is the common interface for interacting with an entire
  3534  * widget group - AKA, all instances of a widget across a series of windows.
  3535  * This particular wrapper is only used for widgets created via the old-school
  3536  * XUL method (overlays, or programmatically injecting toolbaritems, or other
  3537  * such things).
  3538  */
  3539 //XXXunf Going to need to hook this up to some events to keep it all live.
  3540 function XULWidgetGroupWrapper(aWidgetId) {
  3541   this.isGroup = true;
  3542   this.id = aWidgetId;
  3543   this.type = "custom";
  3544   this.provider = CustomizableUI.PROVIDER_XUL;
  3546   this.forWindow = function XULWidgetGroupWrapper_forWindow(aWindow) {
  3547     let wrapperMap;
  3548     if (!gSingleWrapperCache.has(aWindow)) {
  3549       wrapperMap = new Map();
  3550       gSingleWrapperCache.set(aWindow, wrapperMap);
  3551     } else {
  3552       wrapperMap = gSingleWrapperCache.get(aWindow);
  3554     if (wrapperMap.has(aWidgetId)) {
  3555       return wrapperMap.get(aWidgetId);
  3558     let instance = aWindow.document.getElementById(aWidgetId);
  3559     if (!instance) {
  3560       // Toolbar palettes aren't part of the document, so elements in there
  3561       // won't be found via document.getElementById().
  3562       instance = aWindow.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0];
  3565     let wrapper = new XULWidgetSingleWrapper(aWidgetId, instance, aWindow.document);
  3566     wrapperMap.set(aWidgetId, wrapper);
  3567     return wrapper;
  3568   };
  3570   this.__defineGetter__("areaType", function() {
  3571     let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId);
  3572     if (!placement) {
  3573       return null;
  3576     let areaProps = gAreas.get(placement.area);
  3577     return areaProps && areaProps.get("type");
  3578   });
  3580   this.__defineGetter__("instances", function() {
  3581     return [this.forWindow(win) for ([win,] of gBuildWindows)];
  3582   });
  3584   Object.freeze(this);
  3587 /**
  3588  * A XULWidgetSingleWrapper is a wrapper around a single instance of a XUL 
  3589  * widget in a particular window.
  3590  */
  3591 function XULWidgetSingleWrapper(aWidgetId, aNode, aDocument) {
  3592   this.isGroup = false;
  3594   this.id = aWidgetId;
  3595   this.type = "custom";
  3596   this.provider = CustomizableUI.PROVIDER_XUL;
  3598   let weakDoc = Cu.getWeakReference(aDocument);
  3599   // If we keep a strong ref, the weak ref will never die, so null it out:
  3600   aDocument = null;
  3602   this.__defineGetter__("node", function() {
  3603     // If we've set this to null (further down), we're sure there's nothing to
  3604     // be gotten here, so bail out early:
  3605     if (!weakDoc) {
  3606       return null;
  3608     if (aNode) {
  3609       // Return the last known node if it's still in the DOM...
  3610       if (aNode.ownerDocument.contains(aNode)) {
  3611         return aNode;
  3613       // ... or the toolbox
  3614       let toolbox = aNode.ownerDocument.defaultView.gNavToolbox;
  3615       if (toolbox && toolbox.palette && aNode.parentNode == toolbox.palette) {
  3616         return aNode;
  3618       // If it isn't, clear the cached value and fall through to the "slow" case:
  3619       aNode = null;
  3622     let doc = weakDoc.get();
  3623     if (doc) {
  3624       // Store locally so we can cache the result:
  3625       aNode = CustomizableUIInternal.findWidgetInWindow(aWidgetId, doc.defaultView);
  3626       return aNode;
  3628     // The weakref to the document is dead, we're done here forever more:
  3629     weakDoc = null;
  3630     return null;
  3631   });
  3633   this.__defineGetter__("anchor", function() {
  3634     let anchorId;
  3635     // First check for an anchor for the area:
  3636     let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId);
  3637     if (placement) {
  3638       anchorId = gAreas.get(placement.area).get("anchor");
  3641     let node = this.node;
  3642     if (!anchorId && node) {
  3643       anchorId = node.getAttribute("cui-anchorid");
  3646     return (anchorId && node) ? node.ownerDocument.getElementById(anchorId) : node;
  3647   });
  3649   this.__defineGetter__("overflowed", function() {
  3650     let node = this.node;
  3651     if (!node) {
  3652       return false;
  3654     return node.getAttribute("overflowedItem") == "true";
  3655   });
  3657   Object.freeze(this);
  3660 const LAZY_RESIZE_INTERVAL_MS = 200;
  3662 function OverflowableToolbar(aToolbarNode) {
  3663   this._toolbar = aToolbarNode;
  3664   this._collapsed = new Map();
  3665   this._enabled = true;
  3667   this._toolbar.setAttribute("overflowable", "true");
  3668   let doc = this._toolbar.ownerDocument;
  3669   this._target = this._toolbar.customizationTarget;
  3670   this._list = doc.getElementById(this._toolbar.getAttribute("overflowtarget"));
  3671   this._list.toolbox = this._toolbar.toolbox;
  3672   this._list.customizationTarget = this._list;
  3674   let window = this._toolbar.ownerDocument.defaultView;
  3675   if (window.gBrowserInit.delayedStartupFinished) {
  3676     this.init();
  3677   } else {
  3678     Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
  3682 OverflowableToolbar.prototype = {
  3683   initialized: false,
  3684   _forceOnOverflow: false,
  3686   observe: function(aSubject, aTopic, aData) {
  3687     if (aTopic == "browser-delayed-startup-finished" &&
  3688         aSubject == this._toolbar.ownerDocument.defaultView) {
  3689       Services.obs.removeObserver(this, "browser-delayed-startup-finished");
  3690       this.init();
  3692   },
  3694   init: function() {
  3695     let doc = this._toolbar.ownerDocument;
  3696     let window = doc.defaultView;
  3697     window.addEventListener("resize", this);
  3698     window.gNavToolbox.addEventListener("customizationstarting", this);
  3699     window.gNavToolbox.addEventListener("aftercustomization", this);
  3701     let chevronId = this._toolbar.getAttribute("overflowbutton");
  3702     this._chevron = doc.getElementById(chevronId);
  3703     this._chevron.addEventListener("command", this);
  3705     let panelId = this._toolbar.getAttribute("overflowpanel");
  3706     this._panel = doc.getElementById(panelId);
  3707     this._panel.addEventListener("popuphiding", this);
  3708     CustomizableUIInternal.addPanelCloseListeners(this._panel);
  3710     CustomizableUI.addListener(this);
  3712     // The 'overflow' event may have been fired before init was called.
  3713     if (this._toolbar.overflowedDuringConstruction) {
  3714       this.onOverflow(this._toolbar.overflowedDuringConstruction);
  3715       this._toolbar.overflowedDuringConstruction = null;
  3718     this.initialized = true;
  3719   },
  3721   uninit: function() {
  3722     this._toolbar.removeEventListener("overflow", this._toolbar);
  3723     this._toolbar.removeEventListener("underflow", this._toolbar);
  3724     this._toolbar.removeAttribute("overflowable");
  3726     if (!this.initialized) {
  3727       Services.obs.removeObserver(this, "browser-delayed-startup-finished");
  3728       return;
  3731     this._disable();
  3733     let window = this._toolbar.ownerDocument.defaultView;
  3734     window.removeEventListener("resize", this);
  3735     window.gNavToolbox.removeEventListener("customizationstarting", this);
  3736     window.gNavToolbox.removeEventListener("aftercustomization", this);
  3737     this._chevron.removeEventListener("command", this);
  3738     this._panel.removeEventListener("popuphiding", this);
  3739     CustomizableUI.removeListener(this);
  3740     CustomizableUIInternal.removePanelCloseListeners(this._panel);
  3741   },
  3743   handleEvent: function(aEvent) {
  3744     switch(aEvent.type) {
  3745       case "resize":
  3746         this._onResize(aEvent);
  3747         break;
  3748       case "command":
  3749         if (aEvent.target == this._chevron) {
  3750           this._onClickChevron(aEvent);
  3751         } else {
  3752           this._panel.hidePopup();
  3754         break;
  3755       case "popuphiding":
  3756         this._onPanelHiding(aEvent);
  3757         break;
  3758       case "customizationstarting":
  3759         this._disable();
  3760         break;
  3761       case "aftercustomization":
  3762         this._enable();
  3763         break;
  3765   },
  3767   show: function() {
  3768     let deferred = Promise.defer();
  3769     if (this._panel.state == "open") {
  3770       deferred.resolve();
  3771       return deferred.promise;
  3773     let doc = this._panel.ownerDocument;
  3774     this._panel.hidden = false;
  3775     let contextMenu = doc.getElementById(this._panel.getAttribute("context"));
  3776     gELS.addSystemEventListener(contextMenu, 'command', this, true);
  3777     let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon");
  3778     this._panel.openPopup(anchor || this._chevron);
  3779     this._chevron.open = true;
  3781     this._panel.addEventListener("popupshown", function onPopupShown() {
  3782       this.removeEventListener("popupshown", onPopupShown);
  3783       deferred.resolve();
  3784     });
  3786     return deferred.promise;
  3787   },
  3789   _onClickChevron: function(aEvent) {
  3790     if (this._chevron.open) {
  3791       this._panel.hidePopup();
  3792       this._chevron.open = false;
  3793     } else {
  3794       this.show();
  3796   },
  3798   _onPanelHiding: function(aEvent) {
  3799     this._chevron.open = false;
  3800     let doc = aEvent.target.ownerDocument;
  3801     let contextMenu = doc.getElementById(this._panel.getAttribute("context"));
  3802     gELS.removeSystemEventListener(contextMenu, 'command', this, true);
  3803   },
  3805   onOverflow: function(aEvent) {
  3806     if (!this._enabled ||
  3807         (aEvent && aEvent.target != this._toolbar.customizationTarget))
  3808       return;
  3810     let child = this._target.lastChild;
  3812     while (child && this._target.scrollLeftMax > 0) {
  3813       let prevChild = child.previousSibling;
  3815       if (child.getAttribute("overflows") != "false") {
  3816         this._collapsed.set(child.id, this._target.clientWidth);
  3817         child.setAttribute("overflowedItem", true);
  3818         child.setAttribute("cui-anchorid", this._chevron.id);
  3819         CustomizableUIInternal.notifyListeners("onWidgetOverflow", child, this._target);
  3821         this._list.insertBefore(child, this._list.firstChild);
  3822         if (!this._toolbar.hasAttribute("overflowing")) {
  3823           CustomizableUI.addListener(this);
  3825         this._toolbar.setAttribute("overflowing", "true");
  3827       child = prevChild;
  3828     };
  3830     let win = this._target.ownerDocument.defaultView;
  3831     win.UpdateUrlbarSearchSplitterState();
  3832   },
  3834   _onResize: function(aEvent) {
  3835     if (!this._lazyResizeHandler) {
  3836       this._lazyResizeHandler = new DeferredTask(this._onLazyResize.bind(this),
  3837                                                  LAZY_RESIZE_INTERVAL_MS);
  3839     this._lazyResizeHandler.arm();
  3840   },
  3842   _moveItemsBackToTheirOrigin: function(shouldMoveAllItems) {
  3843     let placements = gPlacements.get(this._toolbar.id);
  3844     while (this._list.firstChild) {
  3845       let child = this._list.firstChild;
  3846       let minSize = this._collapsed.get(child.id);
  3848       if (!shouldMoveAllItems &&
  3849           minSize &&
  3850           this._target.clientWidth <= minSize) {
  3851         return;
  3854       this._collapsed.delete(child.id);
  3855       let beforeNodeIndex = placements.indexOf(child.id) + 1;
  3856       // If this is a skipintoolbarset item, meaning it doesn't occur in the placements list,
  3857       // we're inserting it at the end. This will mean first-in, first-out (more or less)
  3858       // leading to as little change in order as possible.
  3859       if (beforeNodeIndex == 0) {
  3860         beforeNodeIndex = placements.length;
  3862       let inserted = false;
  3863       for (; beforeNodeIndex < placements.length; beforeNodeIndex++) {
  3864         let beforeNode = this._target.getElementsByAttribute("id", placements[beforeNodeIndex])[0];
  3865         if (beforeNode) {
  3866           this._target.insertBefore(child, beforeNode);
  3867           inserted = true;
  3868           break;
  3871       if (!inserted) {
  3872         this._target.appendChild(child);
  3874       child.removeAttribute("cui-anchorid");
  3875       child.removeAttribute("overflowedItem");
  3876       CustomizableUIInternal.notifyListeners("onWidgetUnderflow", child, this._target);
  3879     let win = this._target.ownerDocument.defaultView;
  3880     win.UpdateUrlbarSearchSplitterState();
  3882     if (!this._collapsed.size) {
  3883       this._toolbar.removeAttribute("overflowing");
  3884       CustomizableUI.removeListener(this);
  3886   },
  3888   _onLazyResize: function() {
  3889     if (!this._enabled)
  3890       return;
  3892     if (this._target.scrollLeftMax > 0) {
  3893       this.onOverflow();
  3894     } else {
  3895       this._moveItemsBackToTheirOrigin();
  3897   },
  3899   _disable: function() {
  3900     this._enabled = false;
  3901     this._moveItemsBackToTheirOrigin(true);
  3902     if (this._lazyResizeHandler) {
  3903       this._lazyResizeHandler.disarm();
  3905   },
  3907   _enable: function() {
  3908     this._enabled = true;
  3909     this.onOverflow();
  3910   },
  3912   onWidgetBeforeDOMChange: function(aNode, aNextNode, aContainer) {
  3913     if (aContainer != this._target && aContainer != this._list) {
  3914       return;
  3916     // When we (re)move an item, update all the items that come after it in the list
  3917     // with the minsize *of the item before the to-be-removed node*. This way, we
  3918     // ensure that we try to move items back as soon as that's possible.
  3919     if (aNode.parentNode == this._list) {
  3920       let updatedMinSize;
  3921       if (aNode.previousSibling) {
  3922         updatedMinSize = this._collapsed.get(aNode.previousSibling.id);
  3923       } else {
  3924         // Force (these) items to try to flow back into the bar:
  3925         updatedMinSize = 1;
  3927       let nextItem = aNode.nextSibling;
  3928       while (nextItem) {
  3929         this._collapsed.set(nextItem.id, updatedMinSize);
  3930         nextItem = nextItem.nextSibling;
  3933   },
  3935   onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer) {
  3936     if (aContainer != this._target && aContainer != this._list) {
  3937       return;
  3940     let nowInBar = aNode.parentNode == aContainer;
  3941     let nowOverflowed = aNode.parentNode == this._list;
  3942     let wasOverflowed = this._collapsed.has(aNode.id);
  3944     // If this wasn't overflowed before...
  3945     if (!wasOverflowed) {
  3946       // ... but it is now, then we added to the overflow panel. Exciting stuff:
  3947       if (nowOverflowed) {
  3948         // NB: we're guaranteed that it has a previousSibling, because if it didn't,
  3949         // we would have added it to the toolbar instead. See getOverflowedNextNode.
  3950         let prevId = aNode.previousSibling.id;
  3951         let minSize = this._collapsed.get(prevId);
  3952         this._collapsed.set(aNode.id, minSize);
  3953         aNode.setAttribute("cui-anchorid", this._chevron.id);
  3954         aNode.setAttribute("overflowedItem", true);
  3955         CustomizableUIInternal.notifyListeners("onWidgetOverflow", aNode, this._target);
  3957       // If it is not overflowed and not in the toolbar, and was not overflowed
  3958       // either, it moved out of the toolbar. That means there's now space in there!
  3959       // Let's try to move stuff back:
  3960       else if (!nowInBar) {
  3961         this._moveItemsBackToTheirOrigin(true);
  3963       // If it's in the toolbar now, then we don't care. An overflow event may
  3964       // fire afterwards; that's ok!
  3966     // If it used to be overflowed...
  3967     else {
  3968       // ... and isn't anymore, let's remove our bookkeeping:
  3969       if (!nowOverflowed) {
  3970         this._collapsed.delete(aNode.id);
  3971         aNode.removeAttribute("cui-anchorid");
  3972         aNode.removeAttribute("overflowedItem");
  3973         CustomizableUIInternal.notifyListeners("onWidgetUnderflow", aNode, this._target);
  3975         if (!this._collapsed.size) {
  3976           this._toolbar.removeAttribute("overflowing");
  3977           CustomizableUI.removeListener(this);
  3980       // but if it still is, it must have changed places. Bookkeep:
  3981       else {
  3982         if (aNode.previousSibling) {
  3983           let prevId = aNode.previousSibling.id;
  3984           let minSize = this._collapsed.get(prevId);
  3985           this._collapsed.set(aNode.id, minSize);
  3986         } else {
  3987           // If it's now the first item in the overflow list,
  3988           // maybe we can return it:
  3989           this._moveItemsBackToTheirOrigin();
  3993   },
  3995   findOverflowedInsertionPoints: function(aNode) {
  3996     let newNodeCanOverflow = aNode.getAttribute("overflows") != "false";
  3997     let areaId = this._toolbar.id;
  3998     let placements = gPlacements.get(areaId);
  3999     let nodeIndex = placements.indexOf(aNode.id);
  4000     let nodeBeforeNewNodeIsOverflown = false;
  4002     let loopIndex = -1;
  4003     while (++loopIndex < placements.length) {
  4004       let nextNodeId = placements[loopIndex];
  4005       if (loopIndex > nodeIndex) {
  4006         if (newNodeCanOverflow && this._collapsed.has(nextNodeId)) {
  4007           let nextNode = this._list.getElementsByAttribute("id", nextNodeId).item(0);
  4008           if (nextNode) {
  4009             return [this._list, nextNode];
  4012         if (!nodeBeforeNewNodeIsOverflown || !newNodeCanOverflow) {
  4013           let nextNode = this._target.getElementsByAttribute("id", nextNodeId).item(0);
  4014           if (nextNode) {
  4015             return [this._target, nextNode];
  4018       } else if (loopIndex < nodeIndex && this._collapsed.has(nextNodeId)) {
  4019         nodeBeforeNewNodeIsOverflown = true;
  4023     let containerForAppending = (this._collapsed.size && newNodeCanOverflow) ?
  4024                                 this._list : this._target;
  4025     return [containerForAppending, null];
  4026   },
  4028   getContainerFor: function(aNode) {
  4029     if (aNode.getAttribute("overflowedItem") == "true") {
  4030       return this._list;
  4032     return this._target;
  4033   },
  4034 };
  4036 CustomizableUIInternal.initialize();

mercurial