Bumping manifests a=b2g-bump
[gecko.git] / browser / components / customizableui / CustomizableUI.jsm
blob295699689e080089ea97982a8eff682277c874ff
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";
40 const kPrefDeveditionTheme           = "browser.devedition.theme.enabled";
41 const kPrefWebIDEInNavbar            = "devtools.webide.widget.inNavbarByDefault";
43 /**
44  * The keys are the handlers that are fired when the event type (the value)
45  * is fired on the subview. A widget that provides a subview has the option
46  * of providing onViewShowing and onViewHiding event handlers.
47  */
48 const kSubviewEvents = [
49   "ViewShowing",
50   "ViewHiding"
53 /**
54  * The method name to use for ES6 iteration. If Symbols are enabled in
55  * this build, use Symbol.iterator; otherwise "@@iterator".
56  */
57 const JS_HAS_SYMBOLS = typeof Symbol === "function";
58 const kIteratorSymbol = JS_HAS_SYMBOLS ? Symbol.iterator : "@@iterator";
60 /**
61  * The current version. We can use this to auto-add new default widgets as necessary.
62  * (would be const but isn't because of testing purposes)
63  */
64 let kVersion = 4;
66 /**
67  * gPalette is a map of every widget that CustomizableUI.jsm knows about, keyed
68  * on their IDs.
69  */
70 let gPalette = new Map();
72 /**
73  * gAreas maps area IDs to Sets of properties about those areas. An area is a
74  * place where a widget can be put.
75  */
76 let gAreas = new Map();
78 /**
79  * gPlacements maps area IDs to Arrays of widget IDs, indicating that the widgets
80  * are placed within that area (either directly in the area node, or in the
81  * customizationTarget of the node).
82  */
83 let gPlacements = new Map();
85 /**
86  * gFuturePlacements represent placements that will happen for areas that have
87  * not yet loaded (due to lazy-loading). This can occur when add-ons register
88  * widgets.
89  */
90 let gFuturePlacements = new Map();
92 //XXXunf Temporary. Need a nice way to abstract functions to build widgets
93 //       of these types.
94 let gSupportedWidgetTypes = new Set(["button", "view", "custom"]);
96 /**
97  * gPanelsForWindow is a list of known panels in a window which we may need to close
98  * should command events fire which target them.
99  */
100 let gPanelsForWindow = new WeakMap();
103  * gSeenWidgets remembers which widgets the user has seen for the first time
104  * before. This way, if a new widget is created, and the user has not seen it
105  * before, it can be put in its default location. Otherwise, it remains in the
106  * palette.
107  */
108 let gSeenWidgets = new Set();
111  * gDirtyAreaCache is a set of area IDs for areas where items have been added,
112  * moved or removed at least once. This set is persisted, and is used to
113  * optimize building of toolbars in the default case where no toolbars should
114  * be "dirty".
115  */
116 let gDirtyAreaCache = new Set();
119  * gPendingBuildAreas is a map from area IDs to map from build nodes to their
120  * existing children at the time of node registration, that are waiting
121  * for the area to be registered
122  */
123 let gPendingBuildAreas = new Map();
125 let gSavedState = null;
126 let gRestoring = false;
127 let gDirty = false;
128 let gInBatchStack = 0;
129 let gResetting = false;
130 let gUndoResetting = false;
133  * gBuildAreas maps area IDs to actual area nodes within browser windows.
134  */
135 let gBuildAreas = new Map();
138  * gBuildWindows is a map of windows that have registered build areas, mapped
139  * to a Set of known toolboxes in that window.
140  */
141 let gBuildWindows = new Map();
143 let gNewElementCount = 0;
144 let gGroupWrapperCache = new Map();
145 let gSingleWrapperCache = new WeakMap();
146 let gListeners = new Set();
148 let gUIStateBeforeReset = {
149   uiCustomizationState: null,
150   drawInTitlebar: null,
151   gUIStateBeforeReset: null,
154 let gModuleName = "[CustomizableUI]";
155 #include logging.js
157 let CustomizableUIInternal = {
158   initialize: function() {
159     LOG("Initializing");
161     this.addListener(this);
162     this._defineBuiltInWidgets();
163     this.loadSavedState();
164     this._introduceNewBuiltinWidgets();
166     let panelPlacements = [
167       "edit-controls",
168       "zoom-controls",
169       "new-window-button",
170       "privatebrowsing-button",
171       "save-page-button",
172       "print-button",
173       "history-panelmenu",
174       "fullscreen-button",
175       "find-button",
176       "preferences-button",
177       "add-ons-button",
178 #ifndef MOZ_DEV_EDITION
179       "developer-button",
180 #endif
181     ];
183     if (gPalette.has("switch-to-metro-button")) {
184       panelPlacements.push("switch-to-metro-button");
185     }
187 #ifdef E10S_TESTING_ONLY
188     if (gPalette.has("e10s-button")) {
189       let newWindowIndex = panelPlacements.indexOf("new-window-button");
190       if (newWindowIndex > -1) {
191         panelPlacements.splice(newWindowIndex + 1, 0, "e10s-button");
192       }
193     }
194 #endif
196     let showCharacterEncoding = Services.prefs.getComplexValue(
197       "browser.menu.showCharacterEncoding",
198       Ci.nsIPrefLocalizedString
199     ).data;
200     if (showCharacterEncoding == "true") {
201       panelPlacements.push("characterencoding-button");
202     }
204     this.registerArea(CustomizableUI.AREA_PANEL, {
205       anchor: "PanelUI-menu-button",
206       type: CustomizableUI.TYPE_MENU_PANEL,
207       defaultPlacements: panelPlacements
208     }, true);
209     PanelWideWidgetTracker.init();
211     let navbarPlacements = [
212       "urlbar-container",
213       "search-container",
214 #ifdef MOZ_DEV_EDITION
215       "developer-button",
216 #endif
217       "bookmarks-menu-button",
218       "downloads-button",
219       "home-button",
220       "loop-button",
221     ];
223     if (Services.prefs.getBoolPref(kPrefWebIDEInNavbar)) {
224       navbarPlacements.push("webide-button");
225     }
227     this.registerArea(CustomizableUI.AREA_NAVBAR, {
228       legacy: true,
229       type: CustomizableUI.TYPE_TOOLBAR,
230       overflowable: true,
231       defaultPlacements: navbarPlacements,
232       defaultCollapsed: false,
233     }, true);
234 #ifndef XP_MACOSX
235     this.registerArea(CustomizableUI.AREA_MENUBAR, {
236       legacy: true,
237       type: CustomizableUI.TYPE_TOOLBAR,
238       defaultPlacements: [
239         "menubar-items",
240       ],
241       get defaultCollapsed() {
242 #ifdef MENUBAR_CAN_AUTOHIDE
243 #if defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_QT)
244         return true;
245 #else
246         // This is duplicated logic from /browser/base/jar.mn
247         // for win6BrowserOverlay.xul.
248         return Services.appinfo.OS == "WINNT" &&
249                Services.sysinfo.getProperty("version") != "5.1";
250 #endif
251 #endif
252         return false;
253       }
254     }, true);
255 #endif
256     this.registerArea(CustomizableUI.AREA_TABSTRIP, {
257       legacy: true,
258       type: CustomizableUI.TYPE_TOOLBAR,
259       defaultPlacements: [
260         "tabbrowser-tabs",
261         "new-tab-button",
262         "alltabs-button",
263       ],
264       defaultCollapsed: null,
265     }, true);
266     this.registerArea(CustomizableUI.AREA_BOOKMARKS, {
267       legacy: true,
268       type: CustomizableUI.TYPE_TOOLBAR,
269       defaultPlacements: [
270         "personal-bookmarks",
271       ],
272       defaultCollapsed: true,
273     }, true);
275     this.registerArea(CustomizableUI.AREA_ADDONBAR, {
276       type: CustomizableUI.TYPE_TOOLBAR,
277       legacy: true,
278       defaultPlacements: ["addonbar-closebutton", "status-bar"],
279       defaultCollapsed: false,
280     }, true);
281   },
283   get _builtinToolbars() {
284     return new Set([
285       CustomizableUI.AREA_NAVBAR,
286       CustomizableUI.AREA_BOOKMARKS,
287       CustomizableUI.AREA_TABSTRIP,
288       CustomizableUI.AREA_ADDONBAR,
289 #ifndef XP_MACOSX
290       CustomizableUI.AREA_MENUBAR,
291 #endif
292     ]);
293   },
295   _defineBuiltInWidgets: function() {
296     for (let widgetDefinition of CustomizableWidgets) {
297       this.createBuiltinWidget(widgetDefinition);
298     }
299   },
301   _introduceNewBuiltinWidgets: function() {
302     if (!gSavedState || gSavedState.currentVersion >= kVersion) {
303       return;
304     }
306     let currentVersion = gSavedState.currentVersion;
307     for (let [id, widget] of gPalette) {
308       if (widget._introducedInVersion > currentVersion &&
309           widget.defaultArea) {
310         let futurePlacements = gFuturePlacements.get(widget.defaultArea);
311         if (futurePlacements) {
312           futurePlacements.add(id);
313         } else {
314           gFuturePlacements.set(widget.defaultArea, new Set([id]));
315         }
316       }
317     }
319     if (currentVersion < 2) {
320       // Nuke the old 'loop-call-button' out of orbit.
321       CustomizableUI.removeWidgetFromArea("loop-call-button");
322     }
324     if (currentVersion < 4) {
325       CustomizableUI.removeWidgetFromArea("loop-button-throttled");
326     }
327   },
329   wrapWidget: function(aWidgetId) {
330     if (gGroupWrapperCache.has(aWidgetId)) {
331       return gGroupWrapperCache.get(aWidgetId);
332     }
334     let provider = this.getWidgetProvider(aWidgetId);
335     if (!provider) {
336       return null;
337     }
339     if (provider == CustomizableUI.PROVIDER_API) {
340       let widget = gPalette.get(aWidgetId);
341       if (!widget.wrapper) {
342         widget.wrapper = new WidgetGroupWrapper(widget);
343         gGroupWrapperCache.set(aWidgetId, widget.wrapper);
344       }
345       return widget.wrapper;
346     }
348     // PROVIDER_SPECIAL gets treated the same as PROVIDER_XUL.
349     let wrapper = new XULWidgetGroupWrapper(aWidgetId);
350     gGroupWrapperCache.set(aWidgetId, wrapper);
351     return wrapper;
352   },
354   registerArea: function(aName, aProperties, aInternalCaller) {
355     if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
356       throw new Error("Invalid area name");
357     }
359     let areaIsKnown = gAreas.has(aName);
360     let props = areaIsKnown ? gAreas.get(aName) : new Map();
361     const kImmutableProperties = new Set(["type", "legacy", "overflowable"]);
362     for (let key in aProperties) {
363       if (areaIsKnown && kImmutableProperties.has(key) &&
364           props.get(key) != aProperties[key]) {
365         throw new Error("An area cannot change the property for '" + key + "'");
366       }
367       //XXXgijs for special items, we need to make sure they have an appropriate ID
368       // so we aren't perpetually in a non-default state:
369       if (key == "defaultPlacements" && Array.isArray(aProperties[key])) {
370         props.set(key, aProperties[key].map(x => this.isSpecialWidget(x) ? this.ensureSpecialWidgetId(x) : x ));
371       } else {
372         props.set(key, aProperties[key]);
373       }
374     }
375     // Default to a toolbar:
376     if (!props.has("type")) {
377       props.set("type", CustomizableUI.TYPE_TOOLBAR);
378     }
379     if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
380       // Check aProperties instead of props because this check is only interested
381       // in the passed arguments, not the state of a potentially pre-existing area.
382       if (!aInternalCaller && aProperties["defaultCollapsed"]) {
383         throw new Error("defaultCollapsed is only allowed for default toolbars.")
384       }
385       if (!props.has("defaultCollapsed")) {
386         props.set("defaultCollapsed", true);
387       }
388     } else if (props.has("defaultCollapsed")) {
389       throw new Error("defaultCollapsed only applies for TYPE_TOOLBAR areas.");
390     }
391     // Sanity check type:
392     let allTypes = [CustomizableUI.TYPE_TOOLBAR, CustomizableUI.TYPE_MENU_PANEL];
393     if (allTypes.indexOf(props.get("type")) == -1) {
394       throw new Error("Invalid area type " + props.get("type"));
395     }
397     // And to no placements:
398     if (!props.has("defaultPlacements")) {
399       props.set("defaultPlacements", []);
400     }
401     // Sanity check default placements array:
402     if (!Array.isArray(props.get("defaultPlacements"))) {
403       throw new Error("Should provide an array of default placements");
404     }
406     if (!areaIsKnown) {
407       gAreas.set(aName, props);
409       if (props.get("legacy") && !gPlacements.has(aName)) {
410         // Guarantee this area exists in gFuturePlacements, to avoid checking it in
411         // various places elsewhere.
412         if (!gFuturePlacements.has(aName)) {
413           gFuturePlacements.set(aName, new Set());
414         }
415       } else {
416         this.restoreStateForArea(aName);
417       }
419       // If we have pending build area nodes, register all of them
420       if (gPendingBuildAreas.has(aName)) {
421         let pendingNodes = gPendingBuildAreas.get(aName);
422         for (let [pendingNode, existingChildren] of pendingNodes) {
423           this.registerToolbarNode(pendingNode, existingChildren);
424         }
425         gPendingBuildAreas.delete(aName);
426       }
427     }
428   },
430   unregisterArea: function(aName, aDestroyPlacements) {
431     if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
432       throw new Error("Invalid area name");
433     }
434     if (!gAreas.has(aName) && !gPlacements.has(aName)) {
435       throw new Error("Area not registered");
436     }
438     // Move all the widgets out
439     this.beginBatchUpdate();
440     try {
441       let placements = gPlacements.get(aName);
442       if (placements) {
443         // Need to clone this array so removeWidgetFromArea doesn't modify it
444         placements = [...placements];
445         placements.forEach(this.removeWidgetFromArea, this);
446       }
448       // Delete all remaining traces.
449       gAreas.delete(aName);
450       // Only destroy placements when necessary:
451       if (aDestroyPlacements) {
452         gPlacements.delete(aName);
453       } else {
454         // Otherwise we need to re-set them, as removeFromArea will have emptied
455         // them out:
456         gPlacements.set(aName, placements);
457       }
458       gFuturePlacements.delete(aName);
459       let existingAreaNodes = gBuildAreas.get(aName);
460       if (existingAreaNodes) {
461         for (let areaNode of existingAreaNodes) {
462           this.notifyListeners("onAreaNodeUnregistered", aName, areaNode.customizationTarget,
463                                CustomizableUI.REASON_AREA_UNREGISTERED);
464         }
465       }
466       gBuildAreas.delete(aName);
467     } finally {
468       this.endBatchUpdate(true);
469     }
470   },
472   registerToolbarNode: function(aToolbar, aExistingChildren) {
473     let area = aToolbar.id;
474     if (gBuildAreas.has(area) && gBuildAreas.get(area).has(aToolbar)) {
475       return;
476     }
477     let document = aToolbar.ownerDocument;
478     let areaProperties = gAreas.get(area);
480     // If this area is not registered, try to do it automatically:
481     if (!areaProperties) {
482       // If there's no defaultset attribute and this isn't a legacy extra toolbar,
483       // we assume that we should wait for registerArea to be called:
484       if (!aToolbar.hasAttribute("defaultset") &&
485           !aToolbar.hasAttribute("customindex")) {
486         if (!gPendingBuildAreas.has(area)) {
487           gPendingBuildAreas.set(area, new Map());
488         }
489         let pendingNodes = gPendingBuildAreas.get(area);
490         pendingNodes.set(aToolbar, aExistingChildren);
491         return;
492       }
493       let props = {type: CustomizableUI.TYPE_TOOLBAR, legacy: true};
494       let defaultsetAttribute = aToolbar.getAttribute("defaultset") || "";
495       props.defaultPlacements = defaultsetAttribute.split(',').filter(s => s);
496       this.registerArea(area, props);
497       areaProperties = gAreas.get(area);
498     }
500     this.beginBatchUpdate();
501     try {
502       let placements = gPlacements.get(area);
503       if (!placements && areaProperties.has("legacy")) {
504         let legacyState = aToolbar.getAttribute("currentset");
505         if (legacyState) {
506           legacyState = legacyState.split(",").filter(s => s);
507         }
509         // Manually restore the state here, so the legacy state can be converted. 
510         this.restoreStateForArea(area, legacyState);
511         placements = gPlacements.get(area);
512       }
514       // Check that the current children and the current placements match. If
515       // not, mark it as dirty:
516       if (aExistingChildren.length != placements.length ||
517           aExistingChildren.every((id, i) => id == placements[i])) {
518         gDirtyAreaCache.add(area);
519       }
521       if (areaProperties.has("overflowable")) {
522         aToolbar.overflowable = new OverflowableToolbar(aToolbar);
523       }
525       this.registerBuildArea(area, aToolbar);
527       // We only build the toolbar if it's been marked as "dirty". Dirty means
528       // one of the following things:
529       // 1) Items have been added, moved or removed from this toolbar before.
530       // 2) The number of children of the toolbar does not match the length of
531       //    the placements array for that area.
532       //
533       // This notion of being "dirty" is stored in a cache which is persisted
534       // in the saved state.
535       if (gDirtyAreaCache.has(area)) {
536         this.buildArea(area, placements, aToolbar);
537       }
538       this.notifyListeners("onAreaNodeRegistered", area, aToolbar.customizationTarget);
539       aToolbar.setAttribute("currentset", placements.join(","));
540     } finally {
541       this.endBatchUpdate();
542     }
543   },
545   buildArea: function(aArea, aPlacements, aAreaNode) {
546     let document = aAreaNode.ownerDocument;
547     let window = document.defaultView;
548     let inPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(window);
549     let container = aAreaNode.customizationTarget;
550     let areaIsPanel = gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL;
552     if (!container) {
553       throw new Error("Expected area " + aArea
554                       + " to have a customizationTarget attribute.");
555     }
557     // Restore nav-bar visibility since it may have been hidden
558     // through a migration path (bug 938980) or an add-on.
559     if (aArea == CustomizableUI.AREA_NAVBAR) {
560       aAreaNode.collapsed = false;
561     }
563     this.beginBatchUpdate();
565     try {
566       let currentNode = container.firstChild;
567       let placementsToRemove = new Set();
568       for (let id of aPlacements) {
569         while (currentNode && currentNode.getAttribute("skipintoolbarset") == "true") {
570           currentNode = currentNode.nextSibling;
571         }
573         if (currentNode && currentNode.id == id) {
574           currentNode = currentNode.nextSibling;
575           continue;
576         }
578         if (this.isSpecialWidget(id) && areaIsPanel) {
579           placementsToRemove.add(id);
580           continue;
581         }
583         let [provider, node] = this.getWidgetNode(id, window);
584         if (!node) {
585           LOG("Unknown widget: " + id);
586           continue;
587         }
589         // If the placements have items in them which are (now) no longer removable,
590         // we shouldn't be moving them:
591         if (provider == CustomizableUI.PROVIDER_API) {
592           let widgetInfo = gPalette.get(id);
593           if (!widgetInfo.removable && aArea != widgetInfo.defaultArea) {
594             placementsToRemove.add(id);
595             continue;
596           }
597         } else if (provider == CustomizableUI.PROVIDER_XUL &&
598                    node.parentNode != container && !this.isWidgetRemovable(node)) {
599           placementsToRemove.add(id);
600           continue;
601         } // Special widgets are always removable, so no need to check them
603         if (inPrivateWindow && provider == CustomizableUI.PROVIDER_API) {
604           let widget = gPalette.get(id);
605           if (!widget.showInPrivateBrowsing && inPrivateWindow) {
606             continue;
607           }
608         }
610         this.ensureButtonContextMenu(node, aAreaNode);
611         if (node.localName == "toolbarbutton") {
612           if (areaIsPanel) {
613             node.setAttribute("wrap", "true");
614           } else {
615             node.removeAttribute("wrap");
616           }
617         }
619         this.insertWidgetBefore(node, currentNode, container, aArea);
620         if (gResetting) {
621           this.notifyListeners("onWidgetReset", node, container);
622         } else if (gUndoResetting) {
623           this.notifyListeners("onWidgetUndoMove", node, container);
624         }
625       }
627       if (currentNode) {
628         let palette = aAreaNode.toolbox ? aAreaNode.toolbox.palette : null;
629         let limit = currentNode.previousSibling;
630         let node = container.lastChild;
631         while (node && node != limit) {
632           let previousSibling = node.previousSibling;
633           // Nodes opt-in to removability. If they're removable, and we haven't
634           // seen them in the placements array, then we toss them into the palette
635           // if one exists. If no palette exists, we just remove the node. If the
636           // node is not removable, we leave it where it is. However, we can only
637           // safely touch elements that have an ID - both because we depend on
638           // IDs, and because such elements are not intended to be widgets
639           // (eg, titlebar-placeholder elements).
640           if (node.id && node.getAttribute("skipintoolbarset") != "true") {
641             if (this.isWidgetRemovable(node)) {
642               if (palette && !this.isSpecialWidget(node.id)) {
643                 palette.appendChild(node);
644                 this.removeLocationAttributes(node);
645               } else {
646                 container.removeChild(node);
647               }
648             } else {
649               node.setAttribute("removable", false);
650               LOG("Adding non-removable widget to placements of " + aArea + ": " +
651                   node.id);
652               gPlacements.get(aArea).push(node.id);
653               gDirty = true;
654             }
655           }
656           node = previousSibling;
657         }
658       }
660       // If there are placements in here which aren't removable from their original area,
661       // we remove them from this area's placement array. They will (have) be(en) added
662       // to their original area's placements array in the block above this one.
663       if (placementsToRemove.size) {
664         let placementAry = gPlacements.get(aArea);
665         for (let id of placementsToRemove) {
666           let index = placementAry.indexOf(id);
667           placementAry.splice(index, 1);
668         }
669       }
671       if (gResetting) {
672         this.notifyListeners("onAreaReset", aArea, container);
673       }
674     } finally {
675       this.endBatchUpdate();
676     }
677   },
679   addPanelCloseListeners: function(aPanel) {
680     gELS.addSystemEventListener(aPanel, "click", this, false);
681     gELS.addSystemEventListener(aPanel, "keypress", this, false);
682     let win = aPanel.ownerDocument.defaultView;
683     if (!gPanelsForWindow.has(win)) {
684       gPanelsForWindow.set(win, new Set());
685     }
686     gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel));
687   },
689   removePanelCloseListeners: function(aPanel) {
690     gELS.removeSystemEventListener(aPanel, "click", this, false);
691     gELS.removeSystemEventListener(aPanel, "keypress", this, false);
692     let win = aPanel.ownerDocument.defaultView;
693     let panels = gPanelsForWindow.get(win);
694     if (panels) {
695       panels.delete(this._getPanelForNode(aPanel));
696     }
697   },
699   ensureButtonContextMenu: function(aNode, aAreaNode) {
700     const kPanelItemContextMenu = "customizationPanelItemContextMenu";
702     let currentContextMenu = aNode.getAttribute("context") ||
703                              aNode.getAttribute("contextmenu");
704     let place = CustomizableUI.getPlaceForItem(aAreaNode);
705     let contextMenuForPlace = place == "panel" ?
706                                 kPanelItemContextMenu :
707                                 null;
708     if (contextMenuForPlace && !currentContextMenu) {
709       aNode.setAttribute("context", contextMenuForPlace);
710     } else if (currentContextMenu == kPanelItemContextMenu &&
711                contextMenuForPlace != kPanelItemContextMenu) {
712       aNode.removeAttribute("context");
713       aNode.removeAttribute("contextmenu");
714     }
715   },
717   getWidgetProvider: function(aWidgetId) {
718     if (this.isSpecialWidget(aWidgetId)) {
719       return CustomizableUI.PROVIDER_SPECIAL;
720     }
721     if (gPalette.has(aWidgetId)) {
722       return CustomizableUI.PROVIDER_API;
723     }
724     // If this was an API widget that was destroyed, return null:
725     if (gSeenWidgets.has(aWidgetId)) {
726       return null;
727     }
729     // We fall back to the XUL provider, but we don't know for sure (at this
730     // point) whether it exists there either. So the API is technically lying.
731     // Ideally, it would be able to return an error value (or throw an
732     // exception) if it really didn't exist. Our code calling this function
733     // handles that fine, but this is a public API.
734     return CustomizableUI.PROVIDER_XUL;
735   },
737   getWidgetNode: function(aWidgetId, aWindow) {
738     let document = aWindow.document;
740     if (this.isSpecialWidget(aWidgetId)) {
741       let widgetNode = document.getElementById(aWidgetId) ||
742                        this.createSpecialWidget(aWidgetId, document);
743       return [ CustomizableUI.PROVIDER_SPECIAL, widgetNode];
744     }
746     let widget = gPalette.get(aWidgetId);
747     if (widget) {
748       // If we have an instance of this widget already, just use that.
749       if (widget.instances.has(document)) {
750         LOG("An instance of widget " + aWidgetId + " already exists in this "
751             + "document. Reusing.");
752         return [ CustomizableUI.PROVIDER_API,
753                  widget.instances.get(document) ];
754       }
756       return [ CustomizableUI.PROVIDER_API,
757                this.buildWidget(document, widget) ];
758     }
760     LOG("Searching for " + aWidgetId + " in toolbox.");
761     let node = this.findWidgetInWindow(aWidgetId, aWindow);
762     if (node) {
763       return [ CustomizableUI.PROVIDER_XUL, node ];
764     }
766     LOG("No node for " + aWidgetId + " found.");
767     return [null, null];
768   },
770   registerMenuPanel: function(aPanelContents) {
771     if (gBuildAreas.has(CustomizableUI.AREA_PANEL) &&
772         gBuildAreas.get(CustomizableUI.AREA_PANEL).has(aPanelContents)) {
773       return;
774     }
776     let document = aPanelContents.ownerDocument;
778     aPanelContents.toolbox = document.getElementById("navigator-toolbox");
779     aPanelContents.customizationTarget = aPanelContents;
781     this.addPanelCloseListeners(this._getPanelForNode(aPanelContents));
783     let placements = gPlacements.get(CustomizableUI.AREA_PANEL);
784     this.buildArea(CustomizableUI.AREA_PANEL, placements, aPanelContents);
785     this.notifyListeners("onAreaNodeRegistered", CustomizableUI.AREA_PANEL, aPanelContents);
787     for (let child of aPanelContents.children) {
788       if (child.localName != "toolbarbutton") {
789         if (child.localName == "toolbaritem") {
790           this.ensureButtonContextMenu(child, aPanelContents);
791         }
792         continue;
793       }
794       this.ensureButtonContextMenu(child, aPanelContents);
795       child.setAttribute("wrap", "true");
796     }
798     this.registerBuildArea(CustomizableUI.AREA_PANEL, aPanelContents);
799   },
801   onWidgetAdded: function(aWidgetId, aArea, aPosition) {
802     this.insertNode(aWidgetId, aArea, aPosition, true);
804     if (!gResetting) {
805       this._clearPreviousUIState();
806     }
807   },
809   onWidgetRemoved: function(aWidgetId, aArea) {
810     let areaNodes = gBuildAreas.get(aArea);
811     if (!areaNodes) {
812       return;
813     }
815     let area = gAreas.get(aArea);
816     let isToolbar = area.get("type") == CustomizableUI.TYPE_TOOLBAR;
817     let isOverflowable = isToolbar && area.get("overflowable");
818     let showInPrivateBrowsing = gPalette.has(aWidgetId)
819                               ? gPalette.get(aWidgetId).showInPrivateBrowsing
820                               : true;
822     for (let areaNode of areaNodes) {
823       let window = areaNode.ownerDocument.defaultView;
824       if (!showInPrivateBrowsing &&
825           PrivateBrowsingUtils.isWindowPrivate(window)) {
826         continue;
827       }
829       let container = areaNode.customizationTarget;
830       let widgetNode = window.document.getElementById(aWidgetId);
831       if (widgetNode && isOverflowable) {
832         container = areaNode.overflowable.getContainerFor(widgetNode);
833       }
835       if (!widgetNode || !container.contains(widgetNode)) {
836         INFO("Widget " + aWidgetId + " not found, unable to remove from " + aArea);
837         continue;
838       }
840       this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null, container, true);
842       // We remove location attributes here to make sure they're gone too when a
843       // widget is removed from a toolbar to the palette. See bug 930950.
844       this.removeLocationAttributes(widgetNode);
845       // We also need to remove the panel context menu if it's there:
846       this.ensureButtonContextMenu(widgetNode);
847       widgetNode.removeAttribute("wrap");
848       if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) {
849         container.removeChild(widgetNode);
850       } else {
851         areaNode.toolbox.palette.appendChild(widgetNode);
852       }
853       this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null, container, true);
855       if (isToolbar) {
856         areaNode.setAttribute("currentset", gPlacements.get(aArea).join(','));
857       }
859       let windowCache = gSingleWrapperCache.get(window);
860       if (windowCache) {
861         windowCache.delete(aWidgetId);
862       }
863     }
864     if (!gResetting) {
865       this._clearPreviousUIState();
866     }
867   },
869   onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) {
870     this.insertNode(aWidgetId, aArea, aNewPosition);
871     if (!gResetting) {
872       this._clearPreviousUIState();
873     }
874   },
876   onCustomizeEnd: function(aWindow) {
877     this._clearPreviousUIState();
878   },
880   registerBuildArea: function(aArea, aNode) {
881     // We ensure that the window is registered to have its customization data
882     // cleaned up when unloading.
883     let window = aNode.ownerDocument.defaultView;
884     if (window.closed) {
885       return;
886     }
887     this.registerBuildWindow(window);
889     // Also register this build area's toolbox.
890     if (aNode.toolbox) {
891       gBuildWindows.get(window).add(aNode.toolbox);
892     }
894     if (!gBuildAreas.has(aArea)) {
895       gBuildAreas.set(aArea, new Set());
896     }
898     gBuildAreas.get(aArea).add(aNode);
900     // Give a class to all customize targets to be used for styling in Customize Mode
901     let customizableNode = this.getCustomizeTargetForArea(aArea, window);
902     customizableNode.classList.add("customization-target");
903   },
905   registerBuildWindow: function(aWindow) {
906     if (!gBuildWindows.has(aWindow)) {
907       gBuildWindows.set(aWindow, new Set());
909       aWindow.addEventListener("unload", this);
910       aWindow.addEventListener("command", this, true);
912       this.notifyListeners("onWindowOpened", aWindow);
913     }
914   },
916   unregisterBuildWindow: function(aWindow) {
917     aWindow.removeEventListener("unload", this);
918     aWindow.removeEventListener("command", this, true);
919     gPanelsForWindow.delete(aWindow);
920     gBuildWindows.delete(aWindow);
921     gSingleWrapperCache.delete(aWindow);
922     let document = aWindow.document;
924     for (let [areaId, areaNodes] of gBuildAreas) {
925       let areaProperties = gAreas.get(areaId);
926       for (let node of areaNodes) {
927         if (node.ownerDocument == document) {
928           this.notifyListeners("onAreaNodeUnregistered", areaId, node.customizationTarget,
929                                CustomizableUI.REASON_WINDOW_CLOSED);
930           if (areaProperties.has("overflowable")) {
931             node.overflowable.uninit();
932             node.overflowable = null;
933           }
934           areaNodes.delete(node);
935         }
936       }
937     }
939     for (let [,widget] of gPalette) {
940       widget.instances.delete(document);
941       this.notifyListeners("onWidgetInstanceRemoved", widget.id, document);
942     }
944     for (let [area, areaMap] of gPendingBuildAreas) {
945       let toDelete = [];
946       for (let [areaNode, ] of areaMap) {
947         if (areaNode.ownerDocument == document) {
948           toDelete.push(areaNode);
949         }
950       }
951       for (let areaNode of toDelete) {
952         areaMap.delete(toDelete);
953       }
954     }
956     this.notifyListeners("onWindowClosed", aWindow);
957   },
959   setLocationAttributes: function(aNode, aArea) {
960     let props = gAreas.get(aArea);
961     if (!props) {
962       throw new Error("Expected area " + aArea + " to have a properties Map " +
963                       "associated with it.");
964     }
966     aNode.setAttribute("cui-areatype", props.get("type") || "");
967     let anchor = props.get("anchor");
968     if (anchor) {
969       aNode.setAttribute("cui-anchorid", anchor);
970     } else {
971       aNode.removeAttribute("cui-anchorid");
972     }
973   },
975   removeLocationAttributes: function(aNode) {
976     aNode.removeAttribute("cui-areatype");
977     aNode.removeAttribute("cui-anchorid");
978   },
980   insertNode: function(aWidgetId, aArea, aPosition, isNew) {
981     let areaNodes = gBuildAreas.get(aArea);
982     if (!areaNodes) {
983       return;
984     }
986     let placements = gPlacements.get(aArea);
987     if (!placements) {
988       ERROR("Could not find any placements for " + aArea +
989             " when moving a widget.");
990       return;
991     }
993     // Go through each of the nodes associated with this area and move the
994     // widget to the requested location.
995     for (let areaNode of areaNodes) {
996       this.insertNodeInWindow(aWidgetId, areaNode, isNew);
997     }
998   },
1000   insertNodeInWindow: function(aWidgetId, aAreaNode, isNew) {
1001     let window = aAreaNode.ownerDocument.defaultView;
1002     let showInPrivateBrowsing = gPalette.has(aWidgetId)
1003                               ? gPalette.get(aWidgetId).showInPrivateBrowsing
1004                               : true;
1006     if (!showInPrivateBrowsing && PrivateBrowsingUtils.isWindowPrivate(window)) {
1007       return;
1008     }
1010     let [, widgetNode] = this.getWidgetNode(aWidgetId, window);
1011     if (!widgetNode) {
1012       ERROR("Widget '" + aWidgetId + "' not found, unable to move");
1013       return;
1014     }
1016     let areaId = aAreaNode.id;
1017     if (isNew) {
1018       this.ensureButtonContextMenu(widgetNode, aAreaNode);
1019       if (widgetNode.localName == "toolbarbutton" && areaId == CustomizableUI.AREA_PANEL) {
1020         widgetNode.setAttribute("wrap", "true");
1021       }
1022     }
1024     let [insertionContainer, nextNode] = this.findInsertionPoints(widgetNode, aAreaNode);
1025     this.insertWidgetBefore(widgetNode, nextNode, insertionContainer, areaId);
1027     if (gAreas.get(areaId).get("type") == CustomizableUI.TYPE_TOOLBAR) {
1028       aAreaNode.setAttribute("currentset", gPlacements.get(areaId).join(','));
1029     }
1030   },
1032   findInsertionPoints: function(aNode, aAreaNode) {
1033     let areaId = aAreaNode.id;
1034     let props = gAreas.get(areaId);
1036     // For overflowable toolbars, rely on them (because the work is more complicated):
1037     if (props.get("type") == CustomizableUI.TYPE_TOOLBAR && props.get("overflowable")) {
1038       return aAreaNode.overflowable.findOverflowedInsertionPoints(aNode);
1039     }
1041     let container = aAreaNode.customizationTarget;
1042     let placements = gPlacements.get(areaId);
1043     let nodeIndex = placements.indexOf(aNode.id);
1045     while (++nodeIndex < placements.length) {
1046       let nextNodeId = placements[nodeIndex];
1047       let nextNode = container.getElementsByAttribute("id", nextNodeId).item(0);
1049       if (nextNode) {
1050         return [container, nextNode];
1051       }
1052     }
1054     return [container, null];
1055   },
1057   insertWidgetBefore: function(aNode, aNextNode, aContainer, aArea) {
1058     this.notifyListeners("onWidgetBeforeDOMChange", aNode, aNextNode, aContainer);
1059     this.setLocationAttributes(aNode, aArea);
1060     aContainer.insertBefore(aNode, aNextNode);
1061     this.notifyListeners("onWidgetAfterDOMChange", aNode, aNextNode, aContainer);
1062   },
1064   handleEvent: function(aEvent) {
1065     switch (aEvent.type) {
1066       case "command":
1067         if (!this._originalEventInPanel(aEvent)) {
1068           break;
1069         }
1070         aEvent = aEvent.sourceEvent;
1071         // Fall through
1072       case "click":
1073       case "keypress":
1074         this.maybeAutoHidePanel(aEvent);
1075         break;
1076       case "unload":
1077         this.unregisterBuildWindow(aEvent.currentTarget);
1078         break;
1079     }
1080   },
1082   _originalEventInPanel: function(aEvent) {
1083     let e = aEvent.sourceEvent;
1084     if (!e) {
1085       return false;
1086     }
1087     let node = this._getPanelForNode(e.target);
1088     if (!node) {
1089       return false;
1090     }
1091     let win = e.view;
1092     let panels = gPanelsForWindow.get(win);
1093     return !!panels && panels.has(node);
1094   },
1096   isSpecialWidget: function(aId) {
1097     return (aId.startsWith(kSpecialWidgetPfx) ||
1098             aId.startsWith("separator") ||
1099             aId.startsWith("spring") ||
1100             aId.startsWith("spacer"));
1101   },
1103   ensureSpecialWidgetId: function(aId) {
1104     let nodeType = aId.match(/spring|spacer|separator/)[0];
1105     // If the ID we were passed isn't a generated one, generate one now:
1106     if (nodeType == aId) {
1107       // Ids are differentiated through a unique count suffix.
1108       return kSpecialWidgetPfx + aId + (++gNewElementCount);
1109     }
1110     return aId;
1111   },
1113   createSpecialWidget: function(aId, aDocument) {
1114     let nodeName = "toolbar" + aId.match(/spring|spacer|separator/)[0];
1115     let node = aDocument.createElementNS(kNSXUL, nodeName);
1116     node.id = this.ensureSpecialWidgetId(aId);
1117     if (nodeName == "toolbarspring") {
1118       node.flex = 1;
1119     }
1120     return node;
1121   },
1123   /* Find a XUL-provided widget in a window. Don't try to use this
1124    * for an API-provided widget or a special widget.
1125    */
1126   findWidgetInWindow: function(aId, aWindow) {
1127     if (!gBuildWindows.has(aWindow)) {
1128       throw new Error("Build window not registered");
1129     }
1131     if (!aId) {
1132       ERROR("findWidgetInWindow was passed an empty string.");
1133       return null;
1134     }
1136     let document = aWindow.document;
1138     // look for a node with the same id, as the node may be
1139     // in a different toolbar.
1140     let node = document.getElementById(aId);
1141     if (node) {
1142       let parent = node.parentNode;
1143       while (parent && !(parent.customizationTarget ||
1144                          parent == aWindow.gNavToolbox.palette)) {
1145         parent = parent.parentNode;
1146       }
1148       if (parent) {
1149         let nodeInArea = node.parentNode.localName == "toolbarpaletteitem" ?
1150                          node.parentNode : node;
1151         // Check if we're in a customization target, or in the palette:
1152         if ((parent.customizationTarget == nodeInArea.parentNode &&
1153              gBuildWindows.get(aWindow).has(parent.toolbox)) ||
1154             aWindow.gNavToolbox.palette == nodeInArea.parentNode) {
1155           // Normalize the removable attribute. For backwards compat, if
1156           // the widget is not located in a toolbox palette then absence
1157           // of the "removable" attribute means it is not removable.
1158           if (!node.hasAttribute("removable")) {
1159             // If we first see this in customization mode, it may be in the
1160             // customization palette instead of the toolbox palette.
1161             node.setAttribute("removable", !parent.customizationTarget);
1162           }
1163           return node;
1164         }
1165       }
1166     }
1168     let toolboxes = gBuildWindows.get(aWindow);
1169     for (let toolbox of toolboxes) {
1170       if (toolbox.palette) {
1171         // Attempt to locate a node with a matching ID within
1172         // the palette.
1173         let node = toolbox.palette.getElementsByAttribute("id", aId)[0];
1174         if (node) {
1175           // Normalize the removable attribute. For backwards compat, this
1176           // is optional if the widget is located in the toolbox palette,
1177           // and defaults to *true*, unlike if it was located elsewhere.
1178           if (!node.hasAttribute("removable")) {
1179             node.setAttribute("removable", true);
1180           }
1181           return node;
1182         }
1183       }
1184     }
1185     return null;
1186   },
1188   buildWidget: function(aDocument, aWidget) {
1189     if (typeof aWidget == "string") {
1190       aWidget = gPalette.get(aWidget);
1191     }
1192     if (!aWidget) {
1193       throw new Error("buildWidget was passed a non-widget to build.");
1194     }
1196     LOG("Building " + aWidget.id + " of type " + aWidget.type);
1198     let node;
1199     if (aWidget.type == "custom") {
1200       if (aWidget.onBuild) {
1201         node = aWidget.onBuild(aDocument);
1202       }
1203       if (!node || !(node instanceof aDocument.defaultView.XULElement))
1204         ERROR("Custom widget with id " + aWidget.id + " does not return a valid node");
1205     }
1206     else {
1207       if (aWidget.onBeforeCreated) {
1208         aWidget.onBeforeCreated(aDocument);
1209       }
1210       node = aDocument.createElementNS(kNSXUL, "toolbarbutton");
1212       node.setAttribute("id", aWidget.id);
1213       node.setAttribute("widget-id", aWidget.id);
1214       node.setAttribute("widget-type", aWidget.type);
1215       if (aWidget.disabled) {
1216         node.setAttribute("disabled", true);
1217       }
1218       node.setAttribute("removable", aWidget.removable);
1219       node.setAttribute("overflows", aWidget.overflows);
1220       node.setAttribute("label", this.getLocalizedProperty(aWidget, "label"));
1221       let additionalTooltipArguments = [];
1222       if (aWidget.shortcutId) {
1223         let keyEl = aDocument.getElementById(aWidget.shortcutId);
1224         if (keyEl) {
1225           additionalTooltipArguments.push(ShortcutUtils.prettifyShortcut(keyEl));
1226         } else {
1227           ERROR("Key element with id '" + aWidget.shortcutId + "' for widget '" + aWidget.id +
1228                 "' not found!");
1229         }
1230       }
1232       let tooltip = this.getLocalizedProperty(aWidget, "tooltiptext", additionalTooltipArguments);
1233       node.setAttribute("tooltiptext", tooltip);
1234       node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional");
1236       let commandHandler = this.handleWidgetCommand.bind(this, aWidget, node);
1237       node.addEventListener("command", commandHandler, false);
1238       let clickHandler = this.handleWidgetClick.bind(this, aWidget, node);
1239       node.addEventListener("click", clickHandler, false);
1241       // If the widget has a view, and has view showing / hiding listeners,
1242       // hook those up to this widget.
1243       if (aWidget.type == "view") {
1244         LOG("Widget " + aWidget.id + " has a view. Auto-registering event handlers.");
1245         let viewNode = aDocument.getElementById(aWidget.viewId);
1247         if (viewNode) {
1248           // PanelUI relies on the .PanelUI-subView class to be able to show only
1249           // one sub-view at a time.
1250           viewNode.classList.add("PanelUI-subView");
1252           for (let eventName of kSubviewEvents) {
1253             let handler = "on" + eventName;
1254             if (typeof aWidget[handler] == "function") {
1255               viewNode.addEventListener(eventName, aWidget[handler], false);
1256             }
1257           }
1259           LOG("Widget " + aWidget.id + " showing and hiding event handlers set.");
1260         } else {
1261           ERROR("Could not find the view node with id: " + aWidget.viewId +
1262                 ", for widget: " + aWidget.id + ".");
1263         }
1264       }
1266       if (aWidget.onCreated) {
1267         aWidget.onCreated(node);
1268       }
1269     }
1271     aWidget.instances.set(aDocument, node);
1272     return node;
1273   },
1275   getLocalizedProperty: function(aWidget, aProp, aFormatArgs, aDef) {
1276     if (typeof aWidget == "string") {
1277       aWidget = gPalette.get(aWidget);
1278     }
1279     if (!aWidget) {
1280       throw new Error("getLocalizedProperty was passed a non-widget to work with.");
1281     }
1282     let def, name;
1283     // Let widgets pass their own string identifiers or strings, so that
1284     // we can use strings which aren't the default (in case string ids change)
1285     // and so that non-builtin-widgets can also provide labels, tooltips, etc.
1286     if (aWidget[aProp]) {
1287       name = aWidget[aProp];
1288       // By using this as the default, if a widget provides a full string rather
1289       // than a string ID for localization, we will fall back to that string
1290       // and return that.
1291       def = aDef || name;
1292     } else {
1293       name = aWidget.id + "." + aProp;
1294       def = aDef || "";
1295     }
1296     try {
1297       if (Array.isArray(aFormatArgs) && aFormatArgs.length) {
1298         return gWidgetsBundle.formatStringFromName(name, aFormatArgs,
1299           aFormatArgs.length) || def;
1300       }
1301       return gWidgetsBundle.GetStringFromName(name) || def;
1302     } catch(ex) {
1303       if (!def) {
1304         ERROR("Could not localize property '" + name + "'.");
1305       }
1306     }
1307     return def;
1308   },
1310   addShortcut: function(aShortcutNode, aTargetNode) {
1311     if (!aTargetNode)
1312       aTargetNode = aShortcutNode;
1313     let document = aShortcutNode.ownerDocument;
1315     // Detect if we've already been here before.
1316     if (!aTargetNode || aTargetNode.hasAttribute("shortcut"))
1317       return;
1319     let shortcutId = aShortcutNode.getAttribute("key");
1320     let shortcut;
1321     if (shortcutId) {
1322       shortcut = document.getElementById(shortcutId);
1323     } else {
1324       let commandId = aShortcutNode.getAttribute("command");
1325       if (commandId)
1326         shortcut = ShortcutUtils.findShortcut(document.getElementById(commandId));
1327     }
1328     if (!shortcut) {
1329       return;
1330     }
1332     aTargetNode.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(shortcut));
1333   },
1335   handleWidgetCommand: function(aWidget, aNode, aEvent) {
1336     LOG("handleWidgetCommand");
1338     if (aWidget.type == "button") {
1339       if (aWidget.onCommand) {
1340         try {
1341           aWidget.onCommand.call(null, aEvent);
1342         } catch (e) {
1343           ERROR(e);
1344         }
1345       } else {
1346         //XXXunf Need to think this through more, and formalize.
1347         Services.obs.notifyObservers(aNode,
1348                                      "customizedui-widget-command",
1349                                      aWidget.id);
1350       }
1351     } else if (aWidget.type == "view") {
1352       let ownerWindow = aNode.ownerDocument.defaultView;
1353       let area = this.getPlacementOfWidget(aNode.id).area;
1354       let anchor = aNode;
1355       if (area != CustomizableUI.AREA_PANEL) {
1356         let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow);
1357         if (wrapper && wrapper.anchor) {
1358           this.hidePanelForNode(aNode);
1359           anchor = wrapper.anchor;
1360         }
1361       }
1362       ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, area);
1363     }
1364   },
1366   handleWidgetClick: function(aWidget, aNode, aEvent) {
1367     LOG("handleWidgetClick");
1368     if (aWidget.onClick) {
1369       try {
1370         aWidget.onClick.call(null, aEvent);
1371       } catch(e) {
1372         Cu.reportError(e);
1373       }
1374     } else {
1375       //XXXunf Need to think this through more, and formalize.
1376       Services.obs.notifyObservers(aNode, "customizedui-widget-click", aWidget.id);
1377     }
1378   },
1380   _getPanelForNode: function(aNode) {
1381     let panel = aNode;
1382     while (panel && panel.localName != "panel")
1383       panel = panel.parentNode;
1384     return panel;
1385   },
1387   /*
1388    * If people put things in the panel which need more than single-click interaction,
1389    * we don't want to close it. Right now we check for text inputs and menu buttons.
1390    * We also check for being outside of any toolbaritem/toolbarbutton, ie on a blank
1391    * part of the menu.
1392    */
1393   _isOnInteractiveElement: function(aEvent) {
1394     function getMenuPopupForDescendant(aNode) {
1395       let lastPopup = null;
1396       while (aNode && aNode.parentNode &&
1397              aNode.parentNode.localName.startsWith("menu")) {
1398         lastPopup = aNode.localName == "menupopup" ? aNode : lastPopup;
1399         aNode = aNode.parentNode;
1400       }
1401       return lastPopup;
1402     }
1404     let target = aEvent.originalTarget;
1405     let panel = this._getPanelForNode(aEvent.currentTarget);
1406     // This can happen in e.g. customize mode. If there's no panel,
1407     // there's clearly nothing for us to close; pretend we're interactive.
1408     if (!panel) {
1409       return true;
1410     }
1411     // We keep track of:
1412     // whether we're in an input container (text field)
1413     let inInput = false;
1414     // whether we're in a popup/context menu
1415     let inMenu = false;
1416     // whether we're in a toolbarbutton/toolbaritem
1417     let inItem = false;
1418     // whether the current menuitem has a valid closemenu attribute
1419     let menuitemCloseMenu = "auto";
1420     // whether the toolbarbutton/item has a valid closemenu attribute.
1421     let closemenu = "auto";
1423     // While keeping track of that, we go from the original target back up,
1424     // to the panel if we have to. We bail as soon as we find an input,
1425     // a toolbarbutton/item, or the panel:
1426     while (true && target) {
1427       let tagName = target.localName;
1428       inInput = tagName == "input" || tagName == "textbox";
1429       inItem = tagName == "toolbaritem" || tagName == "toolbarbutton";
1430       let isMenuItem = tagName == "menuitem";
1431       inMenu = inMenu || isMenuItem;
1432       if (inItem && target.hasAttribute("closemenu")) {
1433         let closemenuVal = target.getAttribute("closemenu");
1434         closemenu = (closemenuVal == "single" || closemenuVal == "none") ?
1435                     closemenuVal : "auto";
1436       }
1438       if (isMenuItem && target.hasAttribute("closemenu")) {
1439         let closemenuVal = target.getAttribute("closemenu");
1440         menuitemCloseMenu = (closemenuVal == "single" || closemenuVal == "none") ?
1441                             closemenuVal : "auto";
1442       }
1443       // Break out of the loop immediately for disabled items, as we need to
1444       // keep the menu open in that case.
1445       if (target.getAttribute("disabled") == "true") {
1446         return true;
1447       }
1449       // This isn't in the loop condition because we want to break before
1450       // changing |target| if any of these conditions are true
1451       if (inInput || inItem || target == panel) {
1452         break;
1453       }
1454       // We need specific code for popups: the item on which they were invoked
1455       // isn't necessarily in their parentNode chain:
1456       if (isMenuItem) {
1457         let topmostMenuPopup = getMenuPopupForDescendant(target);
1458         target = (topmostMenuPopup && topmostMenuPopup.triggerNode) ||
1459                  target.parentNode;
1460       } else {
1461         target = target.parentNode;
1462       }
1463     }
1465     // If the user clicked a menu item...
1466     if (inMenu) {
1467       // We care if we're in an input also,
1468       // or if the user specified closemenu!="auto":
1469       if (inInput || menuitemCloseMenu != "auto") {
1470         return true;
1471       }
1472       // Otherwise, we're probably fine to close the panel
1473       return false;
1474     }
1475     // If we're not in a menu, and we *are* in a type="menu" toolbarbutton,
1476     // we'll now interact with the menu
1477     if (inItem && target.getAttribute("type") == "menu") {
1478       return true;
1479     }
1480     // If we're not in a menu, and we *are* in a type="menu-button" toolbarbutton,
1481     // it depends whether we're in the dropmarker or the 'real' button:
1482     if (inItem && target.getAttribute("type") == "menu-button") {
1483       // 'real' button (which has a single action):
1484       if (target.getAttribute("anonid") == "button") {
1485         return closemenu != "none";
1486       }
1487       // otherwise, this is the outer button, and the user will now
1488       // interact with the menu:
1489       return true;
1490     }
1491     return inInput || !inItem;
1492   },
1494   hidePanelForNode: function(aNode) {
1495     let panel = this._getPanelForNode(aNode);
1496     if (panel) {
1497       panel.hidePopup();
1498     }
1499   },
1501   maybeAutoHidePanel: function(aEvent) {
1502     if (aEvent.type == "keypress") {
1503       if (aEvent.keyCode != aEvent.DOM_VK_RETURN) {
1504         return;
1505       }
1506       // If the user hit enter/return, we don't check preventDefault - it makes sense
1507       // that this was prevented, but we probably still want to close the panel.
1508       // If consumers don't want this to happen, they should specify the closemenu
1509       // attribute.
1511     } else if (aEvent.type != "command") { // mouse events:
1512       if (aEvent.defaultPrevented || aEvent.button != 0) {
1513         return;
1514       }
1515       let isInteractive = this._isOnInteractiveElement(aEvent);
1516       LOG("maybeAutoHidePanel: interactive ? " + isInteractive);
1517       if (isInteractive) {
1518         return;
1519       }
1520     }
1522     // We can't use event.target because we might have passed a panelview
1523     // anonymous content boundary as well, and so target points to the
1524     // panelmultiview in that case. Unfortunately, this means we get
1525     // anonymous child nodes instead of the real ones, so looking for the 
1526     // 'stoooop, don't close me' attributes is more involved.
1527     let target = aEvent.originalTarget;
1528     let closemenu = "auto";
1529     let widgetType = "button";
1530     while (target.parentNode && target.localName != "panel") {
1531       closemenu = target.getAttribute("closemenu");
1532       widgetType = target.getAttribute("widget-type");
1533       if (closemenu == "none" || closemenu == "single" ||
1534           widgetType == "view") {
1535         break;
1536       }
1537       target = target.parentNode;
1538     }
1539     if (closemenu == "none" || widgetType == "view") {
1540       return;
1541     }
1543     if (closemenu == "single") {
1544       let panel = this._getPanelForNode(target);
1545       let multiview = panel.querySelector("panelmultiview");
1546       if (multiview.showingSubView) {
1547         multiview.showMainView();
1548         return;
1549       }
1550     }
1552     // If we get here, we can actually hide the popup:
1553     this.hidePanelForNode(aEvent.target);
1554   },
1556   getUnusedWidgets: function(aWindowPalette) {
1557     let window = aWindowPalette.ownerDocument.defaultView;
1558     let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
1559     // We use a Set because there can be overlap between the widgets in
1560     // gPalette and the items in the palette, especially after the first
1561     // customization, since programmatically generated widgets will remain
1562     // in the toolbox palette.
1563     let widgets = new Set();
1565     // It's possible that some widgets have been defined programmatically and
1566     // have not been overlayed into the palette. We can find those inside
1567     // gPalette.
1568     for (let [id, widget] of gPalette) {
1569       if (!widget.currentArea) {
1570         if (widget.showInPrivateBrowsing || !isWindowPrivate) {
1571           widgets.add(id);
1572         }
1573       }
1574     }
1576     LOG("Iterating the actual nodes of the window palette");
1577     for (let node of aWindowPalette.children) {
1578       LOG("In palette children: " + node.id);
1579       if (node.id && !this.getPlacementOfWidget(node.id)) {
1580         widgets.add(node.id);
1581       }
1582     }
1584     return [...widgets];
1585   },
1587   getPlacementOfWidget: function(aWidgetId, aOnlyRegistered, aDeadAreas) {
1588     if (aOnlyRegistered && !this.widgetExists(aWidgetId)) {
1589       return null;
1590     }
1592     for (let [area, placements] of gPlacements) {
1593       if (!gAreas.has(area) && !aDeadAreas) {
1594         continue;
1595       }
1596       let index = placements.indexOf(aWidgetId);
1597       if (index != -1) {
1598         return { area: area, position: index };
1599       }
1600     }
1602     return null;
1603   },
1605   widgetExists: function(aWidgetId) {
1606     if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) {
1607       return true;
1608     }
1610     // Destroyed API widgets are in gSeenWidgets, but not in gPalette:
1611     if (gSeenWidgets.has(aWidgetId)) {
1612       return false;
1613     }
1615     // We're assuming XUL widgets always exist, as it's much harder to check,
1616     // and checking would be much more error prone.
1617     return true;
1618   },
1620   addWidgetToArea: function(aWidgetId, aArea, aPosition, aInitialAdd) {
1621     if (!gAreas.has(aArea)) {
1622       throw new Error("Unknown customization area: " + aArea);
1623     }
1625     // Hack: don't want special widgets in the panel (need to check here as well
1626     // as in canWidgetMoveToArea because the menu panel is lazy):
1627     if (gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL &&
1628         this.isSpecialWidget(aWidgetId)) {
1629       return;
1630     }
1632     // If this is a lazy area that hasn't been restored yet, we can't yet modify
1633     // it - would would at least like to add to it. So we keep track of it in
1634     // gFuturePlacements,  and use that to add it when restoring the area. We
1635     // throw away aPosition though, as that can only be bogus if the area hasn't
1636     // yet been restorted (caller can't possibly know where its putting the
1637     // widget in relation to other widgets).
1638     if (this.isAreaLazy(aArea)) {
1639       gFuturePlacements.get(aArea).add(aWidgetId);
1640       return;
1641     }
1643     if (this.isSpecialWidget(aWidgetId)) {
1644       aWidgetId = this.ensureSpecialWidgetId(aWidgetId);
1645     }
1647     let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
1648     if (oldPlacement && oldPlacement.area == aArea) {
1649       this.moveWidgetWithinArea(aWidgetId, aPosition);
1650       return;
1651     }
1653     // Do nothing if the widget is not allowed to move to the target area.
1654     if (!this.canWidgetMoveToArea(aWidgetId, aArea)) {
1655       return;
1656     }
1658     if (oldPlacement) {
1659       this.removeWidgetFromArea(aWidgetId);
1660     }
1662     if (!gPlacements.has(aArea)) {
1663       gPlacements.set(aArea, [aWidgetId]);
1664       aPosition = 0;
1665     } else {
1666       let placements = gPlacements.get(aArea);
1667       if (typeof aPosition != "number") {
1668         aPosition = placements.length;
1669       }
1670       if (aPosition < 0) {
1671         aPosition = 0;
1672       }
1673       placements.splice(aPosition, 0, aWidgetId);
1674     }
1676     let widget = gPalette.get(aWidgetId);
1677     if (widget) {
1678       widget.currentArea = aArea;
1679       widget.currentPosition = aPosition;
1680     }
1682     // We initially set placements with addWidgetToArea, so in that case
1683     // we don't consider the area "dirtied".
1684     if (!aInitialAdd) {
1685       gDirtyAreaCache.add(aArea);
1686     }
1688     gDirty = true;
1689     this.saveState();
1691     this.notifyListeners("onWidgetAdded", aWidgetId, aArea, aPosition);
1692   },
1694   removeWidgetFromArea: function(aWidgetId) {
1695     let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
1696     if (!oldPlacement) {
1697       return;
1698     }
1700     if (!this.isWidgetRemovable(aWidgetId)) {
1701       return;
1702     }
1704     let placements = gPlacements.get(oldPlacement.area);
1705     let position = placements.indexOf(aWidgetId);
1706     if (position != -1) {
1707       placements.splice(position, 1);
1708     }
1710     let widget = gPalette.get(aWidgetId);
1711     if (widget) {
1712       widget.currentArea = null;
1713       widget.currentPosition = null;
1714     }
1716     gDirty = true;
1717     this.saveState();
1718     gDirtyAreaCache.add(oldPlacement.area);
1720     this.notifyListeners("onWidgetRemoved", aWidgetId, oldPlacement.area);
1721   },
1723   moveWidgetWithinArea: function(aWidgetId, aPosition) {
1724     let oldPlacement = this.getPlacementOfWidget(aWidgetId);
1725     if (!oldPlacement) {
1726       return;
1727     }
1729     let placements = gPlacements.get(oldPlacement.area);
1730     if (typeof aPosition != "number") {
1731       aPosition = placements.length;
1732     } else if (aPosition < 0) {
1733       aPosition = 0;
1734     } else if (aPosition > placements.length) {
1735       aPosition = placements.length;
1736     }
1738     let widget = gPalette.get(aWidgetId);
1739     if (widget) {
1740       widget.currentPosition = aPosition;
1741       widget.currentArea = oldPlacement.area;
1742     }
1744     if (aPosition == oldPlacement.position) {
1745       return;
1746     }
1748     placements.splice(oldPlacement.position, 1);
1749     // If we just removed the item from *before* where it is now added,
1750     // we need to compensate the position offset for that:
1751     if (oldPlacement.position < aPosition) {
1752       aPosition--;
1753     }
1754     placements.splice(aPosition, 0, aWidgetId);
1756     gDirty = true;
1757     gDirtyAreaCache.add(oldPlacement.area);
1759     this.saveState();
1761     this.notifyListeners("onWidgetMoved", aWidgetId, oldPlacement.area,
1762                          oldPlacement.position, aPosition);
1763   },
1765   // Note that this does not populate gPlacements, which is done lazily so that
1766   // the legacy state can be migrated, which is only available once a browser
1767   // window is openned.
1768   // The panel area is an exception here, since it has no legacy state and is 
1769   // built lazily - and therefore wouldn't otherwise result in restoring its
1770   // state immediately when a browser window opens, which is important for
1771   // other consumers of this API.
1772   loadSavedState: function() {
1773     let state = null;
1774     try {
1775       state = Services.prefs.getCharPref(kPrefCustomizationState);
1776     } catch (e) {
1777       LOG("No saved state found");
1778       // This will fail if nothing has been customized, so silently fall back to
1779       // the defaults.
1780     }
1782     if (!state) {
1783       return;
1784     }
1785     try {
1786       gSavedState = JSON.parse(state);
1787       if (typeof gSavedState != "object" || gSavedState === null) {
1788         throw "Invalid saved state";
1789       }
1790     } catch(e) {
1791       Services.prefs.clearUserPref(kPrefCustomizationState);
1792       gSavedState = {};
1793       LOG("Error loading saved UI customization state, falling back to defaults.");
1794     }
1796     if (!("placements" in gSavedState)) {
1797       gSavedState.placements = {};
1798     }
1800     if (!("currentVersion" in gSavedState)) {
1801       gSavedState.currentVersion = 0;
1802     }
1804     gSeenWidgets = new Set(gSavedState.seen || []);
1805     gDirtyAreaCache = new Set(gSavedState.dirtyAreaCache || []);
1806     gNewElementCount = gSavedState.newElementCount || 0;
1807   },
1809   restoreStateForArea: function(aArea, aLegacyState) {
1810     let placementsPreexisted = gPlacements.has(aArea);
1812     this.beginBatchUpdate();
1813     try {
1814       gRestoring = true;
1816       let restored = false;
1817       if (placementsPreexisted) {
1818         LOG("Restoring " + aArea + " from pre-existing placements");
1819         for (let [position, id] in Iterator(gPlacements.get(aArea))) {
1820           this.moveWidgetWithinArea(id, position);
1821         }
1822         gDirty = false;
1823         restored = true;
1824       } else {
1825         gPlacements.set(aArea, []);
1826       }
1828       if (!restored && gSavedState && aArea in gSavedState.placements) {
1829         LOG("Restoring " + aArea + " from saved state");
1830         let placements = gSavedState.placements[aArea];
1831         for (let id of placements)
1832           this.addWidgetToArea(id, aArea);
1833         gDirty = false;
1834         restored = true;
1835       }
1837       if (!restored && aLegacyState) {
1838         LOG("Restoring " + aArea + " from legacy state");
1839         for (let id of aLegacyState)
1840           this.addWidgetToArea(id, aArea);
1841         // Don't override dirty state, to ensure legacy state is saved here and
1842         // therefore only used once.
1843         restored = true;
1844       }
1846       if (!restored) {
1847         LOG("Restoring " + aArea + " from default state");
1848         let defaults = gAreas.get(aArea).get("defaultPlacements");
1849         if (defaults) {
1850           for (let id of defaults)
1851             this.addWidgetToArea(id, aArea, null, true);
1852         }
1853         gDirty = false;
1854       }
1856       // Finally, add widgets to the area that were added before the it was able
1857       // to be restored. This can occur when add-ons register widgets for a
1858       // lazily-restored area before it's been restored.
1859       if (gFuturePlacements.has(aArea)) {
1860         for (let id of gFuturePlacements.get(aArea))
1861           this.addWidgetToArea(id, aArea);
1862         gFuturePlacements.delete(aArea);
1863       }
1865       LOG("Placements for " + aArea + ":\n\t" + gPlacements.get(aArea).join("\n\t"));
1867       gRestoring = false;
1868     } finally {
1869       this.endBatchUpdate();
1870     }
1871   },
1873   saveState: function() {
1874     if (gInBatchStack || !gDirty) {
1875       return;
1876     }
1877     // Clone because we want to modify this map:
1878     let state = { placements: new Map(gPlacements),
1879                   seen: gSeenWidgets,
1880                   dirtyAreaCache: gDirtyAreaCache,
1881                   currentVersion: kVersion,
1882                   newElementCount: gNewElementCount };
1884     // Merge in previously saved areas if not present in gPlacements.
1885     // This way, state is still persisted for e.g. temporarily disabled
1886     // add-ons - see bug 989338.
1887     if (gSavedState && gSavedState.placements) {
1888       for (let area of Object.keys(gSavedState.placements)) {
1889         if (!state.placements.has(area)) {
1890           let placements = gSavedState.placements[area];
1891           state.placements.set(area, placements);
1892         }
1893       }
1894     }
1896     LOG("Saving state.");
1897     let serialized = JSON.stringify(state, this.serializerHelper);
1898     LOG("State saved as: " + serialized);
1899     Services.prefs.setCharPref(kPrefCustomizationState, serialized);
1900     gDirty = false;
1901   },
1903   serializerHelper: function(aKey, aValue) {
1904     if (typeof aValue == "object" && aValue.constructor.name == "Map") {
1905       let result = {};
1906       for (let [mapKey, mapValue] of aValue)
1907         result[mapKey] = mapValue;
1908       return result;
1909     }
1911     if (typeof aValue == "object" && aValue.constructor.name == "Set") {
1912       return [...aValue];
1913     }
1915     return aValue;
1916   },
1918   beginBatchUpdate: function() {
1919     gInBatchStack++;
1920   },
1922   endBatchUpdate: function(aForceDirty) {
1923     gInBatchStack--;
1924     if (aForceDirty === true) {
1925       gDirty = true;
1926     }
1927     if (gInBatchStack == 0) {
1928       this.saveState();
1929     } else if (gInBatchStack < 0) {
1930       throw new Error("The batch editing stack should never reach a negative number.");
1931     }
1932   },
1934   addListener: function(aListener) {
1935     gListeners.add(aListener);
1936   },
1938   removeListener: function(aListener) {
1939     if (aListener == this) {
1940       return;
1941     }
1943     gListeners.delete(aListener);
1944   },
1946   notifyListeners: function(aEvent, ...aArgs) {
1947     if (gRestoring) {
1948       return;
1949     }
1951     for (let listener of gListeners) {
1952       try {
1953         if (typeof listener[aEvent] == "function") {
1954           listener[aEvent].apply(listener, aArgs);
1955         }
1956       } catch (e) {
1957         ERROR(e + " -- " + e.fileName + ":" + e.lineNumber);
1958       }
1959     }
1960   },
1962   _dispatchToolboxEventToWindow: function(aEventType, aDetails, aWindow) {
1963     let evt = new aWindow.CustomEvent(aEventType, {
1964       bubbles: true,
1965       cancelable: true,
1966       detail: aDetails
1967     });
1968     aWindow.gNavToolbox.dispatchEvent(evt);
1969   },
1971   dispatchToolboxEvent: function(aEventType, aDetails={}, aWindow=null) {
1972     if (aWindow) {
1973       return this._dispatchToolboxEventToWindow(aEventType, aDetails, aWindow);
1974     }
1975     for (let [win, ] of gBuildWindows) {
1976       this._dispatchToolboxEventToWindow(aEventType, aDetails, win);
1977     }
1978   },
1980   createWidget: function(aProperties) {
1981     let widget = this.normalizeWidget(aProperties, CustomizableUI.SOURCE_EXTERNAL);
1982     //XXXunf This should probably throw.
1983     if (!widget) {
1984       return;
1985     }
1987     gPalette.set(widget.id, widget);
1989     // Clear our caches:
1990     gGroupWrapperCache.delete(widget.id);
1991     for (let [win, ] of gBuildWindows) {
1992       let cache = gSingleWrapperCache.get(win);
1993       if (cache) {
1994         cache.delete(widget.id);
1995       }
1996     }
1998     this.notifyListeners("onWidgetCreated", widget.id);
2000     if (widget.defaultArea) {
2001       let addToDefaultPlacements = false;
2002       let area = gAreas.get(widget.defaultArea);
2003       if (!CustomizableUI.isBuiltinToolbar(widget.defaultArea) &&
2004           widget.defaultArea != CustomizableUI.AREA_PANEL) {
2005         addToDefaultPlacements = true;
2006       }
2008       if (addToDefaultPlacements) {
2009         if (area.has("defaultPlacements")) {
2010           area.get("defaultPlacements").push(widget.id);
2011         } else {
2012           area.set("defaultPlacements", [widget.id]);
2013         }
2014       }
2015     }
2017     // Look through previously saved state to see if we're restoring a widget.
2018     let seenAreas = new Set();
2019     let widgetMightNeedAutoAdding = true;
2020     for (let [area, placements] of gPlacements) {
2021       seenAreas.add(area);
2022       let areaIsRegistered = gAreas.has(area);
2023       let index = gPlacements.get(area).indexOf(widget.id);
2024       if (index != -1) {
2025         widgetMightNeedAutoAdding = false;
2026         if (areaIsRegistered) {
2027           widget.currentArea = area;
2028           widget.currentPosition = index;
2029         }
2030         break;
2031       }
2032     }
2034     // Also look at saved state data directly in areas that haven't yet been
2035     // restored. Can't rely on this for restored areas, as they may have
2036     // changed.
2037     if (widgetMightNeedAutoAdding && gSavedState) {
2038       for (let area of Object.keys(gSavedState.placements)) {
2039         if (seenAreas.has(area)) {
2040           continue;
2041         }
2043         let areaIsRegistered = gAreas.has(area);
2044         let index = gSavedState.placements[area].indexOf(widget.id);
2045         if (index != -1) {
2046           widgetMightNeedAutoAdding = false;
2047           if (areaIsRegistered) {
2048             widget.currentArea = area;
2049             widget.currentPosition = index;
2050           }
2051           break;
2052         }
2053       }
2054     }
2056     // If we're restoring the widget to it's old placement, fire off the
2057     // onWidgetAdded event - our own handler will take care of adding it to
2058     // any build areas.
2059     this.beginBatchUpdate();
2060     try {
2061       if (widget.currentArea) {
2062         this.notifyListeners("onWidgetAdded", widget.id, widget.currentArea,
2063                              widget.currentPosition);
2064       } else if (widgetMightNeedAutoAdding) {
2065         let autoAdd = true;
2066         try {
2067           autoAdd = Services.prefs.getBoolPref(kPrefCustomizationAutoAdd);
2068         } catch (e) {}
2070         // If the widget doesn't have an existing placement, and it hasn't been
2071         // seen before, then add it to its default area so it can be used.
2072         // If the widget is not removable, we *have* to add it to its default
2073         // area here.
2074         let canBeAutoAdded = autoAdd && !gSeenWidgets.has(widget.id);
2075         if (!widget.currentArea && (!widget.removable || canBeAutoAdded)) {
2076           if (widget.defaultArea) {
2077             if (this.isAreaLazy(widget.defaultArea)) {
2078               gFuturePlacements.get(widget.defaultArea).add(widget.id);
2079             } else {
2080               this.addWidgetToArea(widget.id, widget.defaultArea);
2081             }
2082           }
2083         }
2084       }
2085     } finally {
2086       // Ensure we always have this widget in gSeenWidgets, and save
2087       // state in case this needs to be done here.
2088       gSeenWidgets.add(widget.id);
2089       this.endBatchUpdate(true);
2090     }
2092     this.notifyListeners("onWidgetAfterCreation", widget.id, widget.currentArea);
2093     return widget.id;
2094   },
2096   createBuiltinWidget: function(aData) {
2097     // This should only ever be called on startup, before any windows are
2098     // opened - so we know there's no build areas to handle. Also, builtin
2099     // widgets are expected to be (mostly) static, so shouldn't affect the
2100     // current placement settings.
2101     let widget = this.normalizeWidget(aData, CustomizableUI.SOURCE_BUILTIN);
2102     if (!widget) {
2103       ERROR("Error creating builtin widget: " + aData.id);
2104       return;
2105     }
2107     LOG("Creating built-in widget with id: " + widget.id);
2108     gPalette.set(widget.id, widget);
2109   },
2111   // Returns true if the area will eventually lazily restore (but hasn't yet).
2112   isAreaLazy: function(aArea) {
2113     if (gPlacements.has(aArea)) {
2114       return false;
2115     }
2116     return gAreas.get(aArea).has("legacy");
2117   },
2119   //XXXunf Log some warnings here, when the data provided isn't up to scratch.
2120   normalizeWidget: function(aData, aSource) {
2121     let widget = {
2122       implementation: aData,
2123       source: aSource || CustomizableUI.SOURCE_EXTERNAL,
2124       instances: new Map(),
2125       currentArea: null,
2126       removable: true,
2127       overflows: true,
2128       defaultArea: null,
2129       shortcutId: null,
2130       tooltiptext: null,
2131       showInPrivateBrowsing: true,
2132       _introducedInVersion: -1,
2133     };
2135     if (typeof aData.id != "string" || !/^[a-z0-9-_]{1,}$/i.test(aData.id)) {
2136       ERROR("Given an illegal id in normalizeWidget: " + aData.id);
2137       return null;
2138     }
2140     delete widget.implementation.currentArea;
2141     widget.implementation.__defineGetter__("currentArea", function() widget.currentArea);
2143     const kReqStringProps = ["id"];
2144     for (let prop of kReqStringProps) {
2145       if (typeof aData[prop] != "string") {
2146         ERROR("Missing required property '" + prop + "' in normalizeWidget: "
2147               + aData.id);
2148         return null;
2149       }
2150       widget[prop] = aData[prop];
2151     }
2153     const kOptStringProps = ["label", "tooltiptext", "shortcutId"];
2154     for (let prop of kOptStringProps) {
2155       if (typeof aData[prop] == "string") {
2156         widget[prop] = aData[prop];
2157       }
2158     }
2160     const kOptBoolProps = ["removable", "showInPrivateBrowsing", "overflows"];
2161     for (let prop of kOptBoolProps) {
2162       if (typeof aData[prop] == "boolean") {
2163         widget[prop] = aData[prop];
2164       }
2165     }
2167     // When we normalize builtin widgets, areas have not yet been registered:
2168     if (aData.defaultArea &&
2169         (aSource == CustomizableUI.SOURCE_BUILTIN || gAreas.has(aData.defaultArea))) {
2170       widget.defaultArea = aData.defaultArea;
2171     } else if (!widget.removable) {
2172       ERROR("Widget '" + widget.id + "' is not removable but does not specify " +
2173             "a valid defaultArea. That's not possible; it must specify a " +
2174             "valid defaultArea as well.");
2175       return null;
2176     }
2178     if ("type" in aData && gSupportedWidgetTypes.has(aData.type)) {
2179       widget.type = aData.type;
2180     } else {
2181       widget.type = "button";
2182     }
2184     widget.disabled = aData.disabled === true;
2186     if (aSource == CustomizableUI.SOURCE_BUILTIN) {
2187       widget._introducedInVersion = aData.introducedInVersion || 0;
2188     }
2190     this.wrapWidgetEventHandler("onBeforeCreated", widget);
2191     this.wrapWidgetEventHandler("onClick", widget);
2192     this.wrapWidgetEventHandler("onCreated", widget);
2194     if (widget.type == "button") {
2195       widget.onCommand = typeof aData.onCommand == "function" ?
2196                            aData.onCommand :
2197                            null;
2198     } else if (widget.type == "view") {
2199       if (typeof aData.viewId != "string") {
2200         ERROR("Expected a string for widget " + widget.id + " viewId, but got "
2201               + aData.viewId);
2202         return null;
2203       }
2204       widget.viewId = aData.viewId;
2206       this.wrapWidgetEventHandler("onViewShowing", widget);
2207       this.wrapWidgetEventHandler("onViewHiding", widget);
2208     } else if (widget.type == "custom") {
2209       this.wrapWidgetEventHandler("onBuild", widget);
2210     }
2212     if (gPalette.has(widget.id)) {
2213       return null;
2214     }
2216     return widget;
2217   },
2219   wrapWidgetEventHandler: function(aEventName, aWidget) {
2220     if (typeof aWidget.implementation[aEventName] != "function") {
2221       aWidget[aEventName] = null;
2222       return;
2223     }
2224     aWidget[aEventName] = function(...aArgs) {
2225       // Wrap inside a try...catch to properly log errors, until bug 862627 is
2226       // fixed, which in turn might help bug 503244.
2227       try {
2228         // Don't copy the function to the normalized widget object, instead
2229         // keep it on the original object provided to the API so that
2230         // additional methods can be implemented and used by the event
2231         // handlers.
2232         return aWidget.implementation[aEventName].apply(aWidget.implementation,
2233                                                         aArgs);
2234       } catch (e) {
2235         Cu.reportError(e);
2236       }
2237     };
2238   },
2240   destroyWidget: function(aWidgetId) {
2241     let widget = gPalette.get(aWidgetId);
2242     if (!widget) {
2243       gGroupWrapperCache.delete(aWidgetId);
2244       for (let [window, ] of gBuildWindows) {
2245         let windowCache = gSingleWrapperCache.get(window);
2246         if (windowCache) {
2247           windowCache.delete(aWidgetId);
2248         }
2249       }
2250       return;
2251     }
2253     // Remove it from the default placements of an area if it was added there:
2254     if (widget.defaultArea) {
2255       let area = gAreas.get(widget.defaultArea);
2256       if (area) {
2257         let defaultPlacements = area.get("defaultPlacements");
2258         // We can assume this is present because if a widget has a defaultArea,
2259         // we automatically create a defaultPlacements array for that area.
2260         let widgetIndex = defaultPlacements.indexOf(aWidgetId);
2261         if (widgetIndex != -1) {
2262           defaultPlacements.splice(widgetIndex, 1);
2263         }
2264       }
2265     }
2267     // This will not remove the widget from gPlacements - we want to keep the
2268     // setting so the widget gets put back in it's old position if/when it
2269     // returns.
2270     for (let [window, ] of gBuildWindows) {
2271       let windowCache = gSingleWrapperCache.get(window);
2272       if (windowCache) {
2273         windowCache.delete(aWidgetId);
2274       }
2275       let widgetNode = window.document.getElementById(aWidgetId) ||
2276                        window.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0];
2277       if (widgetNode) {
2278         let container = widgetNode.parentNode
2279         this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null,
2280                              container, true);
2281         widgetNode.remove();
2282         this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null,
2283                              container, true);
2284       }
2285       if (widget.type == "view") {
2286         let viewNode = window.document.getElementById(widget.viewId);
2287         if (viewNode) {
2288           for (let eventName of kSubviewEvents) {
2289             let handler = "on" + eventName;
2290             if (typeof widget[handler] == "function") {
2291               viewNode.removeEventListener(eventName, widget[handler], false);
2292             }
2293           }
2294         }
2295       }
2296     }
2298     gPalette.delete(aWidgetId);
2299     gGroupWrapperCache.delete(aWidgetId);
2301     this.notifyListeners("onWidgetDestroyed", aWidgetId);
2302   },
2304   getCustomizeTargetForArea: function(aArea, aWindow) {
2305     let buildAreaNodes = gBuildAreas.get(aArea);
2306     if (!buildAreaNodes) {
2307       return null;
2308     }
2310     for (let node of buildAreaNodes) {
2311       if (node.ownerDocument.defaultView === aWindow) {
2312         return node.customizationTarget ? node.customizationTarget : node;
2313       }
2314     }
2316     return null;
2317   },
2319   reset: function() {
2320     gResetting = true;
2321     this._resetUIState();
2323     // Rebuild each registered area (across windows) to reflect the state that
2324     // was reset above.
2325     this._rebuildRegisteredAreas();
2327     for (let [widgetId, widget] of gPalette) {
2328       if (widget.source == CustomizableUI.SOURCE_EXTERNAL) {
2329         gSeenWidgets.add(widgetId);
2330       }
2331     }
2332     if (gSeenWidgets.size) {
2333       gDirty = true;
2334     }
2336     gResetting = false;
2337   },
2339   _resetUIState: function() {
2340     try {
2341       gUIStateBeforeReset.drawInTitlebar = Services.prefs.getBoolPref(kPrefDrawInTitlebar);
2342       gUIStateBeforeReset.deveditionTheme = Services.prefs.getBoolPref(kPrefDeveditionTheme);
2343       gUIStateBeforeReset.uiCustomizationState = Services.prefs.getCharPref(kPrefCustomizationState);
2344     } catch(e) { }
2346     this._resetExtraToolbars();
2348     Services.prefs.clearUserPref(kPrefCustomizationState);
2349     Services.prefs.clearUserPref(kPrefDrawInTitlebar);
2350     Services.prefs.clearUserPref(kPrefDeveditionTheme);
2351     LOG("State reset");
2353     // Reset placements to make restoring default placements possible.
2354     gPlacements = new Map();
2355     gDirtyAreaCache = new Set();
2356     gSeenWidgets = new Set();
2357     // Clear the saved state to ensure that defaults will be used.
2358     gSavedState = null;
2359     // Restore the state for each area to its defaults
2360     for (let [areaId,] of gAreas) {
2361       this.restoreStateForArea(areaId);
2362     }
2363   },
2365   _resetExtraToolbars: function(aFilter = null) {
2366     let firstWindow = true; // Only need to unregister and persist once
2367     for (let [win, ] of gBuildWindows) {
2368       let toolbox = win.gNavToolbox;
2369       for (let child of toolbox.children) {
2370         let matchesFilter = !aFilter || aFilter == child.id;
2371         if (child.hasAttribute("customindex") && matchesFilter) {
2372           let toolbarId = "toolbar" + child.getAttribute("customindex");
2373           toolbox.toolbarset.removeAttribute(toolbarId);
2374           if (firstWindow) {
2375             win.document.persist(toolbox.toolbarset.id, toolbarId);
2376             // We have to unregister it properly to ensure we don't kill
2377             // XUL widgets which might be in here
2378             this.unregisterArea(child.id, true);
2379           }
2380           child.remove();
2381         }
2382       }
2383       firstWindow = false;
2384     }
2385   },
2387   _rebuildRegisteredAreas: function() {
2388     for (let [areaId, areaNodes] of gBuildAreas) {
2389       let placements = gPlacements.get(areaId);
2390       let isFirstChangedToolbar = true;
2391       for (let areaNode of areaNodes) {
2392         this.buildArea(areaId, placements, areaNode);
2394         let area = gAreas.get(areaId);
2395         if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) {
2396           let defaultCollapsed = area.get("defaultCollapsed");
2397           let win = areaNode.ownerDocument.defaultView;
2398           if (defaultCollapsed !== null) {
2399             win.setToolbarVisibility(areaNode, !defaultCollapsed, isFirstChangedToolbar);
2400           }
2401         }
2402         isFirstChangedToolbar = false;
2403       }
2404     }
2405   },
2407   /**
2408    * Undoes a previous reset, restoring the state of the UI to the state prior to the reset.
2409    */
2410   undoReset: function() {
2411     if (gUIStateBeforeReset.uiCustomizationState == null ||
2412         gUIStateBeforeReset.drawInTitlebar == null ||
2413         gUIStateBeforeReset.deveditionTheme == null) {
2414       return;
2415     }
2416     gUndoResetting = true;
2418     let uiCustomizationState = gUIStateBeforeReset.uiCustomizationState;
2419     let drawInTitlebar = gUIStateBeforeReset.drawInTitlebar;
2420     let deveditionTheme = gUIStateBeforeReset.deveditionTheme;
2422     // Need to clear the previous state before setting the prefs
2423     // because pref observers may check if there is a previous UI state.
2424     this._clearPreviousUIState();
2426     Services.prefs.setCharPref(kPrefCustomizationState, uiCustomizationState);
2427     Services.prefs.setBoolPref(kPrefDrawInTitlebar, drawInTitlebar);
2428     Services.prefs.setBoolPref(kPrefDeveditionTheme, deveditionTheme);
2429     this.loadSavedState();
2430     // If the user just customizes toolbar/titlebar visibility, gSavedState will be null
2431     // and we don't need to do anything else here:
2432     if (gSavedState) {
2433       for (let areaId of Object.keys(gSavedState.placements)) {
2434         let placements = gSavedState.placements[areaId];
2435         gPlacements.set(areaId, placements);
2436       }
2437       this._rebuildRegisteredAreas();
2438     }
2440     gUndoResetting = false;
2441   },
2443   _clearPreviousUIState: function() {
2444     Object.getOwnPropertyNames(gUIStateBeforeReset).forEach((prop) => {
2445       gUIStateBeforeReset[prop] = null;
2446     });
2447   },
2449   removeExtraToolbar: function(aToolbarId) {
2450     this._resetExtraToolbars(aToolbarId);
2451   },
2453   /**
2454    * @param {String|Node} aWidget - widget ID or a widget node (preferred for performance).
2455    * @return {Boolean} whether the widget is removable
2456    */
2457   isWidgetRemovable: function(aWidget) {
2458     let widgetId;
2459     let widgetNode;
2460     if (typeof aWidget == "string") {
2461       widgetId = aWidget;
2462     } else {
2463       widgetId = aWidget.id;
2464       widgetNode = aWidget;
2465     }
2466     let provider = this.getWidgetProvider(widgetId);
2468     if (provider == CustomizableUI.PROVIDER_API) {
2469       return gPalette.get(widgetId).removable;
2470     }
2472     if (provider == CustomizableUI.PROVIDER_XUL) {
2473       if (gBuildWindows.size == 0) {
2474         // We don't have any build windows to look at, so just assume for now
2475         // that its removable.
2476         return true;
2477       }
2479       if (!widgetNode) {
2480         // Pick any of the build windows to look at.
2481         let [window,] = [...gBuildWindows][0];
2482         [, widgetNode] = this.getWidgetNode(widgetId, window);
2483       }
2484       // If we don't have a node, we assume it's removable. This can happen because
2485       // getWidgetProvider returns PROVIDER_XUL by default, but this will also happen
2486       // for API-provided widgets which have been destroyed.
2487       if (!widgetNode) {
2488         return true;
2489       }
2490       return widgetNode.getAttribute("removable") == "true";
2491     }
2493     // Otherwise this is either a special widget, which is always removable, or
2494     // an API widget which has already been removed from gPalette. Returning true
2495     // here allows us to then remove its ID from any placements where it might
2496     // still occur.
2497     return true;
2498   },
2500   canWidgetMoveToArea: function(aWidgetId, aArea) {
2501     let placement = this.getPlacementOfWidget(aWidgetId);
2502     if (placement && placement.area != aArea) {
2503       // Special widgets can't move to the menu panel.
2504       if (this.isSpecialWidget(aWidgetId) && gAreas.has(aArea) &&
2505           gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL) {
2506         return false;
2507       }
2508       // For everything else, just return whether the widget is removable.
2509       return this.isWidgetRemovable(aWidgetId);
2510     }
2512     return true;
2513   },
2515   ensureWidgetPlacedInWindow: function(aWidgetId, aWindow) {
2516     let placement = this.getPlacementOfWidget(aWidgetId);
2517     if (!placement) {
2518       return false;
2519     }
2520     let areaNodes = gBuildAreas.get(placement.area);
2521     if (!areaNodes) {
2522       return false;
2523     }
2524     let container = [...areaNodes].filter((n) => n.ownerDocument.defaultView == aWindow);
2525     if (!container.length) {
2526       return false;
2527     }
2528     let existingNode = container[0].getElementsByAttribute("id", aWidgetId)[0];
2529     if (existingNode) {
2530       return true;
2531     }
2533     this.insertNodeInWindow(aWidgetId, container[0], true);
2534     return true;
2535   },
2537   get inDefaultState() {
2538     for (let [areaId, props] of gAreas) {
2539       let defaultPlacements = props.get("defaultPlacements");
2540       // Areas without default placements (like legacy ones?) get skipped
2541       if (!defaultPlacements) {
2542         continue;
2543       }
2545       let currentPlacements = gPlacements.get(areaId);
2546       // We're excluding all of the placement IDs for items that do not exist,
2547       // and items that have removable="false",
2548       // because we don't want to consider them when determining if we're
2549       // in the default state. This way, if an add-on introduces a widget
2550       // and is then uninstalled, the leftover placement doesn't cause us to
2551       // automatically assume that the buttons are not in the default state.
2552       let buildAreaNodes = gBuildAreas.get(areaId);
2553       if (buildAreaNodes && buildAreaNodes.size) {
2554         let container = [...buildAreaNodes][0];
2555         let removableOrDefault = (itemNodeOrItem) => {
2556           let item = (itemNodeOrItem && itemNodeOrItem.id) || itemNodeOrItem;
2557           let isRemovable = this.isWidgetRemovable(itemNodeOrItem);
2558           let isInDefault = defaultPlacements.indexOf(item) != -1;
2559           return isRemovable || isInDefault;
2560         };
2561         // Toolbars have a currentSet property which also deals correctly with overflown
2562         // widgets (if any) - use that instead:
2563         if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
2564           let currentSet = container.currentSet;
2565           currentPlacements = currentSet ? currentSet.split(',') : [];
2566           currentPlacements = currentPlacements.filter(removableOrDefault);
2567         } else {
2568           // Clone the array so we don't modify the actual placements...
2569           currentPlacements = [...currentPlacements];
2570           currentPlacements = currentPlacements.filter((item) => {
2571             let itemNode = container.getElementsByAttribute("id", item)[0];
2572             return itemNode && removableOrDefault(itemNode || item);
2573           });
2574         }
2576         if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
2577           let attribute = container.getAttribute("type") == "menubar" ? "autohide" : "collapsed";
2578           let collapsed = container.getAttribute(attribute) == "true";
2579           let defaultCollapsed = props.get("defaultCollapsed");
2580           if (defaultCollapsed !== null && collapsed != defaultCollapsed) {
2581             LOG("Found " + areaId + " had non-default toolbar visibility (expected " + defaultCollapsed + ", was " + collapsed + ")");
2582             return false;
2583           }
2584         }
2585       }
2586       LOG("Checking default state for " + areaId + ":\n" + currentPlacements.join(",") +
2587           "\nvs.\n" + defaultPlacements.join(","));
2589       if (currentPlacements.length != defaultPlacements.length) {
2590         return false;
2591       }
2593       for (let i = 0; i < currentPlacements.length; ++i) {
2594         if (currentPlacements[i] != defaultPlacements[i]) {
2595           LOG("Found " + currentPlacements[i] + " in " + areaId + " where " +
2596               defaultPlacements[i] + " was expected!");
2597           return false;
2598         }
2599       }
2600     }
2602     if (Services.prefs.prefHasUserValue(kPrefDrawInTitlebar)) {
2603       LOG(kPrefDrawInTitlebar + " pref is non-default");
2604       return false;
2605     }
2606     if (Services.prefs.prefHasUserValue(kPrefDeveditionTheme)) {
2607       LOG(kPrefDeveditionTheme + " pref is non-default");
2608       return false;
2609     }
2611     return true;
2612   },
2614   setToolbarVisibility: function(aToolbarId, aIsVisible) {
2615     // We only persist the attribute the first time.
2616     let isFirstChangedToolbar = true;
2617     for (let window of CustomizableUI.windows) {
2618       let toolbar = window.document.getElementById(aToolbarId);
2619       if (toolbar) {
2620         window.setToolbarVisibility(toolbar, aIsVisible, isFirstChangedToolbar);
2621         isFirstChangedToolbar = false;
2622       }
2623     }
2624   },
2626 Object.freeze(CustomizableUIInternal);
2628 this.CustomizableUI = {
2629   /**
2630    * Constant reference to the ID of the menu panel.
2631    */
2632   get AREA_PANEL() "PanelUI-contents",
2633   /**
2634    * Constant reference to the ID of the navigation toolbar.
2635    */
2636   get AREA_NAVBAR() "nav-bar",
2637   /**
2638    * Constant reference to the ID of the menubar's toolbar.
2639    */
2640   get AREA_MENUBAR() "toolbar-menubar",
2641   /**
2642    * Constant reference to the ID of the tabstrip toolbar.
2643    */
2644   get AREA_TABSTRIP() "TabsToolbar",
2645   /**
2646    * Constant reference to the ID of the bookmarks toolbar.
2647    */
2648   get AREA_BOOKMARKS() "PersonalToolbar",
2649   /**
2650    * Constant reference to the ID of the addon-bar toolbar shim.
2651    * Do not use, this will be removed as soon as reasonably possible.
2652    * @deprecated
2653    */
2654   get AREA_ADDONBAR() "addon-bar",
2655   /**
2656    * Constant indicating the area is a menu panel.
2657    */
2658   get TYPE_MENU_PANEL() "menu-panel",
2659   /**
2660    * Constant indicating the area is a toolbar.
2661    */
2662   get TYPE_TOOLBAR() "toolbar",
2664   /**
2665    * Constant indicating a XUL-type provider.
2666    */
2667   get PROVIDER_XUL() "xul",
2668   /**
2669    * Constant indicating an API-type provider.
2670    */
2671   get PROVIDER_API() "api",
2672   /**
2673    * Constant indicating dynamic (special) widgets: spring, spacer, and separator.
2674    */
2675   get PROVIDER_SPECIAL() "special",
2677   /**
2678    * Constant indicating the widget is built-in
2679    */
2680   get SOURCE_BUILTIN() "builtin",
2681   /**
2682    * Constant indicating the widget is externally provided
2683    * (e.g. by add-ons or other items not part of the builtin widget set).
2684    */
2685   get SOURCE_EXTERNAL() "external",
2687   /**
2688    * The class used to distinguish items that span the entire menu panel.
2689    */
2690   get WIDE_PANEL_CLASS() "panel-wide-item",
2691   /**
2692    * The (constant) number of columns in the menu panel.
2693    */
2694   get PANEL_COLUMN_COUNT() 3,
2696   /**
2697    * Constant indicating the reason the event was fired was a window closing
2698    */
2699   get REASON_WINDOW_CLOSED() "window-closed",
2700   /**
2701    * Constant indicating the reason the event was fired was an area being
2702    * unregistered separately from window closing mechanics.
2703    */
2704   get REASON_AREA_UNREGISTERED() "area-unregistered",
2707   /**
2708    * An iteratable property of windows managed by CustomizableUI.
2709    * Note that this can *only* be used as an iterator. ie:
2710    *     for (let window of CustomizableUI.windows) { ... }
2711    */
2712   windows: {
2713     *[kIteratorSymbol]() {
2714       for (let [window,] of gBuildWindows)
2715         yield window;
2716     }
2717   },
2719   /**
2720    * Add a listener object that will get fired for various events regarding
2721    * customization.
2722    *
2723    * @param aListener the listener object to add
2724    *
2725    * Not all event handler methods need to be defined.
2726    * CustomizableUI will catch exceptions. Events are dispatched
2727    * synchronously on the UI thread, so if you can delay any/some of your
2728    * processing, that is advisable. The following event handlers are supported:
2729    *   - onWidgetAdded(aWidgetId, aArea, aPosition)
2730    *     Fired when a widget is added to an area. aWidgetId is the widget that
2731    *     was added, aArea the area it was added to, and aPosition the position
2732    *     in which it was added.
2733    *   - onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition)
2734    *     Fired when a widget is moved within its area. aWidgetId is the widget
2735    *     that was moved, aArea the area it was moved in, aOldPosition its old
2736    *     position, and aNewPosition its new position.
2737    *   - onWidgetRemoved(aWidgetId, aArea)
2738    *     Fired when a widget is removed from its area. aWidgetId is the widget
2739    *     that was removed, aArea the area it was removed from.
2740    *
2741    *   - onWidgetBeforeDOMChange(aNode, aNextNode, aContainer, aIsRemoval)
2742    *     Fired *before* a widget's DOM node is acted upon by CustomizableUI
2743    *     (to add, move or remove it). aNode is the DOM node changed, aNextNode
2744    *     the DOM node (if any) before which a widget will be inserted,
2745    *     aContainer the *actual* DOM container (could be an overflow panel in
2746    *     case of an overflowable toolbar), and aWasRemoval is true iff the
2747    *     action about to happen is the removal of the DOM node.
2748    *   - onWidgetAfterDOMChange(aNode, aNextNode, aContainer, aWasRemoval)
2749    *     Like onWidgetBeforeDOMChange, but fired after the change to the DOM
2750    *     node of the widget.
2751    *
2752    *   - onWidgetReset(aNode, aContainer)
2753    *     Fired after a reset to default placements moves a widget's node to a
2754    *     different location. aNode is the widget's node, aContainer is the
2755    *     area it was moved into (NB: it might already have been there and been
2756    *     moved to a different position!)
2757    *   - onWidgetUndoMove(aNode, aContainer)
2758    *     Fired after undoing a reset to default placements moves a widget's
2759    *     node to a different location. aNode is the widget's node, aContainer
2760    *     is the area it was moved into (NB: it might already have been there
2761    *     and been moved to a different position!)
2762    *   - onAreaReset(aArea, aContainer)
2763    *     Fired after a reset to default placements is complete on an area's
2764    *     DOM node. Note that this is fired for each DOM node. aArea is the area
2765    *     that was reset, aContainer the DOM node that was reset.
2766    *
2767    *   - onWidgetCreated(aWidgetId)
2768    *     Fired when a widget with id aWidgetId has been created, but before it
2769    *     is added to any placements or any DOM nodes have been constructed.
2770    *     Only fired for API-based widgets.
2771    *   - onWidgetAfterCreation(aWidgetId, aArea)
2772    *     Fired after a widget with id aWidgetId has been created, and has been
2773    *     added to either its default area or the area in which it was placed
2774    *     previously. If the widget has no default area and/or it has never
2775    *     been placed anywhere, aArea may be null. Only fired for API-based
2776    *     widgets.
2777    *   - onWidgetDestroyed(aWidgetId)
2778    *     Fired when widgets are destroyed. aWidgetId is the widget that is
2779    *     being destroyed. Only fired for API-based widgets.
2780    *   - onWidgetInstanceRemoved(aWidgetId, aDocument)
2781    *     Fired when a window is unloaded and a widget's instance is destroyed
2782    *     because of this. Only fired for API-based widgets.
2783    *
2784    *   - onWidgetDrag(aWidgetId, aArea)
2785    *     Fired both when and after customize mode drag handling system tries
2786    *     to determine the width and height of widget aWidgetId when dragged to a
2787    *     different area. aArea will be the area the item is dragged to, or
2788    *     undefined after the measurements have been done and the node has been
2789    *     moved back to its 'regular' area.
2790    *
2791    *   - onCustomizeStart(aWindow)
2792    *     Fired when opening customize mode in aWindow.
2793    *   - onCustomizeEnd(aWindow)
2794    *     Fired when exiting customize mode in aWindow.
2795    *
2796    *   - onWidgetOverflow(aNode, aContainer)
2797    *     Fired when a widget's DOM node is overflowing its container, a toolbar,
2798    *     and will be displayed in the overflow panel.
2799    *   - onWidgetUnderflow(aNode, aContainer)
2800    *     Fired when a widget's DOM node is *not* overflowing its container, a
2801    *     toolbar, anymore.
2802    *   - onWindowOpened(aWindow)
2803    *     Fired when a window has been opened that is managed by CustomizableUI,
2804    *     once all of the prerequisite setup has been done.
2805    *   - onWindowClosed(aWindow)
2806    *     Fired when a window that has been managed by CustomizableUI has been
2807    *     closed.
2808    *   - onAreaNodeRegistered(aArea, aContainer)
2809    *     Fired after an area node is first built when it is registered. This
2810    *     is often when the window has opened, but in the case of add-ons,
2811    *     could fire when the node has just been registered with CustomizableUI
2812    *     after an add-on update or disable/enable sequence.
2813    *   - onAreaNodeUnregistered(aArea, aContainer, aReason)
2814    *     Fired when an area node is explicitly unregistered by an API caller,
2815    *     or by a window closing. The aReason parameter indicates which of
2816    *     these is the case.
2817    */
2818   addListener: function(aListener) {
2819     CustomizableUIInternal.addListener(aListener);
2820   },
2821   /**
2822    * Remove a listener added with addListener
2823    * @param aListener the listener object to remove
2824    */
2825   removeListener: function(aListener) {
2826     CustomizableUIInternal.removeListener(aListener);
2827   },
2829   /**
2830    * Register a customizable area with CustomizableUI.
2831    * @param aName   the name of the area to register. Can only contain
2832    *                alphanumeric characters, dashes (-) and underscores (_).
2833    * @param aProps  the properties of the area. The following properties are
2834    *                recognized:
2835    *                - type:   the type of area. Either TYPE_TOOLBAR (default) or
2836    *                          TYPE_MENU_PANEL;
2837    *                - anchor: for a menu panel or overflowable toolbar, the
2838    *                          anchoring node for the panel.
2839    *                - legacy: set to true if you want customizableui to
2840    *                          automatically migrate the currentset attribute
2841    *                - overflowable: set to true if your toolbar is overflowable.
2842    *                                This requires an anchor, and only has an
2843    *                                effect for toolbars.
2844    *                - defaultPlacements: an array of widget IDs making up the
2845    *                                     default contents of the area
2846    *                - defaultCollapsed: (INTERNAL ONLY) applies if the type is TYPE_TOOLBAR, specifies
2847    *                                    if toolbar is collapsed by default (default to true).
2848    *                                    Specify null to ensure that reset/inDefaultArea don't care
2849    *                                    about a toolbar's collapsed state
2850    */
2851   registerArea: function(aName, aProperties) {
2852     CustomizableUIInternal.registerArea(aName, aProperties);
2853   },
2854   /**
2855    * Register a concrete node for a registered area. This method is automatically
2856    * called from any toolbar in the main browser window that has its
2857    * "customizable" attribute set to true. There should normally be no need to
2858    * call it yourself.
2859    *
2860    * Note that ideally, you should register your toolbar using registerArea
2861    * before any of the toolbars have their XBL bindings constructed (which
2862    * will happen when they're added to the DOM and are not hidden). If you
2863    * don't, and your toolbar has a defaultset attribute, CustomizableUI will
2864    * register it automatically. If your toolbar does not have a defaultset
2865    * attribute, the node will be saved for processing when you call
2866    * registerArea. Note that CustomizableUI won't restore state in the area,
2867    * allow the user to customize it in customize mode, or otherwise deal
2868    * with it, until the area has been registered.
2869    */
2870   registerToolbarNode: function(aToolbar, aExistingChildren) {
2871     CustomizableUIInternal.registerToolbarNode(aToolbar, aExistingChildren);
2872   },
2873   /**
2874    * Register the menu panel node. This method should not be called by anyone
2875    * apart from the built-in PanelUI.
2876    * @param aPanel the panel DOM node being registered.
2877    */
2878   registerMenuPanel: function(aPanel) {
2879     CustomizableUIInternal.registerMenuPanel(aPanel);
2880   },
2881   /**
2882    * Unregister a customizable area. The inverse of registerArea.
2883    *
2884    * Unregistering an area will remove all the (removable) widgets in the
2885    * area, which will return to the panel, and destroy all other traces
2886    * of the area within CustomizableUI. Note that this means the *contents*
2887    * of the area's DOM nodes will be moved to the panel or removed, but
2888    * the area's DOM nodes *themselves* will stay.
2889    *
2890    * Furthermore, by default the placements of the area will be kept in the
2891    * saved state (!) and restored if you re-register the area at a later
2892    * point. This is useful for e.g. add-ons that get disabled and then
2893    * re-enabled (e.g. when they update).
2894    *
2895    * You can override this last behaviour (and destroy the placements
2896    * information in the saved state) by passing true for aDestroyPlacements.
2897    *
2898    * @param aName              the name of the area to unregister
2899    * @param aDestroyPlacements whether to destroy the placements information
2900    *                           for the area, too.
2901    */
2902   unregisterArea: function(aName, aDestroyPlacements) {
2903     CustomizableUIInternal.unregisterArea(aName, aDestroyPlacements);
2904   },
2905   /**
2906    * Add a widget to an area.
2907    * If the area to which you try to add is not known to CustomizableUI,
2908    * this will throw.
2909    * If the area to which you try to add has not yet been restored from its
2910    * legacy state, this will postpone the addition.
2911    * If the area to which you try to add is the same as the area in which
2912    * the widget is currently placed, this will do the same as
2913    * moveWidgetWithinArea.
2914    * If the widget cannot be removed from its original location, this will
2915    * no-op.
2916    *
2917    * This will fire an onWidgetAdded notification,
2918    * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification
2919    * for each window CustomizableUI knows about.
2920    *
2921    * @param aWidgetId the ID of the widget to add
2922    * @param aArea     the ID of the area to add the widget to
2923    * @param aPosition the position at which to add the widget. If you do not
2924    *                  pass a position, the widget will be added to the end
2925    *                  of the area.
2926    */
2927   addWidgetToArea: function(aWidgetId, aArea, aPosition) {
2928     CustomizableUIInternal.addWidgetToArea(aWidgetId, aArea, aPosition);
2929   },
2930   /**
2931    * Remove a widget from its area. If the widget cannot be removed from its
2932    * area, or is not in any area, this will no-op. Otherwise, this will fire an
2933    * onWidgetRemoved notification, and an onWidgetBeforeDOMChange and
2934    * onWidgetAfterDOMChange notification for each window CustomizableUI knows
2935    * about.
2936    *
2937    * @param aWidgetId the ID of the widget to remove
2938    */
2939   removeWidgetFromArea: function(aWidgetId) {
2940     CustomizableUIInternal.removeWidgetFromArea(aWidgetId);
2941   },
2942   /**
2943    * Move a widget within an area.
2944    * If the widget is not in any area, this will no-op.
2945    * If the widget is already at the indicated position, this will no-op.
2946    *
2947    * Otherwise, this will move the widget and fire an onWidgetMoved notification,
2948    * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification for
2949    * each window CustomizableUI knows about.
2950    *
2951    * @param aWidgetId the ID of the widget to move
2952    * @param aPosition the position to move the widget to.
2953    *                  Negative values or values greater than the number of
2954    *                  widgets will be interpreted to mean moving the widget to
2955    *                  respectively the first or last position.
2956    */
2957   moveWidgetWithinArea: function(aWidgetId, aPosition) {
2958     CustomizableUIInternal.moveWidgetWithinArea(aWidgetId, aPosition);
2959   },
2960   /**
2961    * Ensure a XUL-based widget created in a window after areas were
2962    * initialized moves to its correct position.
2963    * This is roughly equivalent to manually looking up the position and using
2964    * insertItem in the old API, but a lot less work for consumers.
2965    * Always prefer this over using toolbar.insertItem (which might no-op
2966    * because it delegates to addWidgetToArea) or, worse, moving items in the
2967    * DOM yourself.
2968    *
2969    * @param aWidgetId the ID of the widget that was just created
2970    * @param aWindow the window in which you want to ensure it was added.
2971    *
2972    * NB: why is this API per-window, you wonder? Because if you need this,
2973    * presumably you yourself need to create the widget in all the windows
2974    * and need to loop through them anyway.
2975    */
2976   ensureWidgetPlacedInWindow: function(aWidgetId, aWindow) {
2977     return CustomizableUIInternal.ensureWidgetPlacedInWindow(aWidgetId, aWindow);
2978   },
2979   /**
2980    * Start a batch update of items.
2981    * During a batch update, the customization state is not saved to the user's
2982    * preferences file, in order to reduce (possibly sync) IO.
2983    * Calls to begin/endBatchUpdate may be nested.
2984    *
2985    * Callers should ensure that NO MATTER WHAT they call endBatchUpdate once
2986    * for each call to beginBatchUpdate, even if there are exceptions in the
2987    * code in the batch update. Otherwise, for the duration of the
2988    * Firefox session, customization state is never saved. Typically, you
2989    * would do this using a try...finally block.
2990    */
2991   beginBatchUpdate: function() {
2992     CustomizableUIInternal.beginBatchUpdate();
2993   },
2994   /**
2995    * End a batch update. See the documentation for beginBatchUpdate above.
2996    *
2997    * State is not saved if we believe it is identical to the last known
2998    * saved state. State is only ever saved when all batch updates have
2999    * finished (ie there has been 1 endBatchUpdate call for each
3000    * beginBatchUpdate call). If any of the endBatchUpdate calls pass
3001    * aForceDirty=true, we will flush to the prefs file.
3002    *
3003    * @param aForceDirty force CustomizableUI to flush to the prefs file when
3004    *                    all batch updates have finished.
3005    */
3006   endBatchUpdate: function(aForceDirty) {
3007     CustomizableUIInternal.endBatchUpdate(aForceDirty);
3008   },
3009   /**
3010    * Create a widget.
3011    *
3012    * To create a widget, you should pass an object with its desired
3013    * properties. The following properties are supported:
3014    *
3015    * - id:            the ID of the widget (required).
3016    * - type:          a string indicating the type of widget. Possible types
3017    *                  are:
3018    *                  'button' - for simple button widgets (the default)
3019    *                  'view'   - for buttons that open a panel or subview,
3020    *                             depending on where they are placed.
3021    *                  'custom' - for fine-grained control over the creation
3022    *                             of the widget.
3023    * - viewId:        Only useful for views (and required there): the id of the
3024    *                  <panelview> that should be shown when clicking the widget.
3025    * - onBuild(aDoc): Only useful for custom widgets (and required there); a
3026    *                  function that will be invoked with the document in which
3027    *                  to build a widget. Should return the DOM node that has
3028    *                  been constructed.
3029    * - onBeforeCreated(aDoc): Attached to all non-custom widgets; a function
3030    *                  that will be invoked before the widget gets a DOM node
3031    *                  constructed, passing the document in which that will happen.
3032    *                  This is useful especially for 'view' type widgets that need
3033    *                  to construct their views on the fly (e.g. from bootstrapped
3034    *                  add-ons)
3035    * - onCreated(aNode): Attached to all widgets; a function that will be invoked
3036    *                  whenever the widget has a DOM node constructed, passing the
3037    *                  constructed node as an argument.
3038    * - onCommand(aEvt): Only useful for button widgets; a function that will be
3039    *                    invoked when the user activates the button.
3040    * - onClick(aEvt): Attached to all widgets; a function that will be invoked
3041    *                  when the user clicks the widget.
3042    * - onViewShowing(aEvt): Only useful for views; a function that will be
3043    *                  invoked when a user shows your view.
3044    * - onViewHiding(aEvt): Only useful for views; a function that will be
3045    *                  invoked when a user hides your view.
3046    * - tooltiptext:   string to use for the tooltip of the widget
3047    * - label:         string to use for the label of the widget
3048    * - removable:     whether the widget is removable (optional, default: true)
3049    *                  NB: if you specify false here, you must provide a
3050    *                  defaultArea, too.
3051    * - overflows:     whether widget can overflow when in an overflowable
3052    *                  toolbar (optional, default: true)
3053    * - defaultArea:   default area to add the widget to
3054    *                  (optional, default: none; required if non-removable)
3055    * - shortcutId:    id of an element that has a shortcut for this widget
3056    *                  (optional, default: null). This is only used to display
3057    *                  the shortcut as part of the tooltip for builtin widgets
3058    *                  (which have strings inside
3059    *                  customizableWidgets.properties). If you're in an add-on,
3060    *                  you should not set this property.
3061    * - showInPrivateBrowsing: whether to show the widget in private browsing
3062    *                          mode (optional, default: true)
3063    *
3064    * @param aProperties the specifications for the widget.
3065    * @return a wrapper around the created widget (see getWidget)
3066    */
3067   createWidget: function(aProperties) {
3068     return CustomizableUIInternal.wrapWidget(
3069       CustomizableUIInternal.createWidget(aProperties)
3070     );
3071   },
3072   /**
3073    * Destroy a widget
3074    *
3075    * If the widget is part of the default placements in an area, this will
3076    * remove it from there. It will also remove any DOM instances. However,
3077    * it will keep the widget in the placements for whatever area it was
3078    * in at the time. You can remove it from there yourself by calling
3079    * CustomizableUI.removeWidgetFromArea(aWidgetId).
3080    *
3081    * @param aWidgetId the ID of the widget to destroy
3082    */
3083   destroyWidget: function(aWidgetId) {
3084     CustomizableUIInternal.destroyWidget(aWidgetId);
3085   },
3086   /**
3087    * Get a wrapper object with information about the widget.
3088    * The object provides the following properties
3089    * (all read-only unless otherwise indicated):
3090    *
3091    * - id:            the widget's ID;
3092    * - type:          the type of widget (button, view, custom). For
3093    *                  XUL-provided widgets, this is always 'custom';
3094    * - provider:      the provider type of the widget, id est one of
3095    *                  PROVIDER_API or PROVIDER_XUL;
3096    * - forWindow(w):  a method to obtain a single window wrapper for a widget,
3097    *                  in the window w passed as the only argument;
3098    * - instances:     an array of all instances (single window wrappers)
3099    *                  of the widget. This array is NOT live;
3100    * - areaType:      the type of the widget's current area
3101    * - isGroup:       true; will be false for wrappers around single widget nodes;
3102    * - source:        for API-provided widgets, whether they are built-in to
3103    *                  Firefox or add-on-provided;
3104    * - disabled:      for API-provided widgets, whether the widget is currently
3105    *                  disabled. NB: this property is writable, and will toggle
3106    *                  all the widgets' nodes' disabled states;
3107    * - label:         for API-provied widgets, the label of the widget;
3108    * - tooltiptext:   for API-provided widgets, the tooltip of the widget;
3109    * - showInPrivateBrowsing: for API-provided widgets, whether the widget is
3110    *                          visible in private browsing;
3111    *
3112    * Single window wrappers obtained through forWindow(someWindow) or from the
3113    * instances array have the following properties
3114    * (all read-only unless otherwise indicated):
3115    *
3116    * - id:            the widget's ID;
3117    * - type:          the type of widget (button, view, custom). For
3118    *                  XUL-provided widgets, this is always 'custom';
3119    * - provider:      the provider type of the widget, id est one of
3120    *                  PROVIDER_API or PROVIDER_XUL;
3121    * - node:          reference to the corresponding DOM node;
3122    * - anchor:        the anchor on which to anchor panels opened from this
3123    *                  node. This will point to the overflow chevron on
3124    *                  overflowable toolbars if and only if your widget node
3125    *                  is overflowed, to the anchor for the panel menu
3126    *                  if your widget is inside the panel menu, and to the
3127    *                  node itself in all other cases;
3128    * - overflowed:    boolean indicating whether the node is currently in the
3129    *                  overflow panel of the toolbar;
3130    * - isGroup:       false; will be true for the group widget;
3131    * - label:         for API-provided widgets, convenience getter for the
3132    *                  label attribute of the DOM node;
3133    * - tooltiptext:   for API-provided widgets, convenience getter for the
3134    *                  tooltiptext attribute of the DOM node;
3135    * - disabled:      for API-provided widgets, convenience getter *and setter*
3136    *                  for the disabled state of this single widget. Note that
3137    *                  you may prefer to use the group wrapper's getter/setter
3138    *                  instead.
3139    *
3140    * @param aWidgetId the ID of the widget whose information you need
3141    * @return a wrapper around the widget as described above, or null if the
3142    *         widget is known not to exist (anymore). NB: non-null return
3143    *         is no guarantee the widget exists because we cannot know in
3144    *         advance if a XUL widget exists or not.
3145    */
3146   getWidget: function(aWidgetId) {
3147     return CustomizableUIInternal.wrapWidget(aWidgetId);
3148   },
3149   /**
3150    * Get an array of widget wrappers (see getWidget) for all the widgets
3151    * which are currently not in any area (so which are in the palette).
3152    *
3153    * @param aWindowPalette the palette (and by extension, the window) in which
3154    *                       CustomizableUI should look. This matters because of
3155    *                       course XUL-provided widgets could be available in
3156    *                       some windows but not others, and likewise
3157    *                       API-provided widgets might not exist in a private
3158    *                       window (because of the showInPrivateBrowsing
3159    *                       property).
3160    *
3161    * @return an array of widget wrappers (see getWidget)
3162    */
3163   getUnusedWidgets: function(aWindowPalette) {
3164     return CustomizableUIInternal.getUnusedWidgets(aWindowPalette).map(
3165       CustomizableUIInternal.wrapWidget,
3166       CustomizableUIInternal
3167     );
3168   },
3169   /**
3170    * Get an array of all the widget IDs placed in an area. This is roughly
3171    * equivalent to fetching the currentset attribute and splitting by commas
3172    * in the legacy APIs. Modifying the array will not affect CustomizableUI.
3173    *
3174    * @param aArea the ID of the area whose placements you want to obtain.
3175    * @return an array containing the widget IDs that are in the area.
3176    *
3177    * NB: will throw if called too early (before placements have been fetched)
3178    *     or if the area is not currently known to CustomizableUI.
3179    */
3180   getWidgetIdsInArea: function(aArea) {
3181     if (!gAreas.has(aArea)) {
3182       throw new Error("Unknown customization area: " + aArea);
3183     }
3184     if (!gPlacements.has(aArea)) {
3185       throw new Error("Area not yet restored");
3186     }
3188     // We need to clone this, as we don't want to let consumers muck with placements
3189     return [...gPlacements.get(aArea)];
3190   },
3191   /**
3192    * Get an array of widget wrappers for all the widgets in an area. This is
3193    * the same as calling getWidgetIdsInArea and .map() ing the result through
3194    * CustomizableUI.getWidget. Careful: this means that if there are IDs in there
3195    * which don't have corresponding DOM nodes (like in the old-style currentset
3196    * attribute), there might be nulls in this array, or items for which
3197    * wrapper.forWindow(win) will return null.
3198    *
3199    * @param aArea the ID of the area whose widgets you want to obtain.
3200    * @return an array of widget wrappers and/or null values for the widget IDs
3201    *         placed in an area.
3202    *
3203    * NB: will throw if called too early (before placements have been fetched)
3204    *     or if the area is not currently known to CustomizableUI.
3205    */
3206   getWidgetsInArea: function(aArea) {
3207     return this.getWidgetIdsInArea(aArea).map(
3208       CustomizableUIInternal.wrapWidget,
3209       CustomizableUIInternal
3210     );
3211   },
3212   /**
3213    * Obtain an array of all the area IDs known to CustomizableUI.
3214    * This array is created for you, so is modifiable without CustomizableUI
3215    * being affected.
3216    */
3217   get areas() {
3218     return [area for ([area, props] of gAreas)];
3219   },
3220   /**
3221    * Check what kind of area (toolbar or menu panel) an area is. This is
3222    * useful if you have a widget that needs to behave differently depending
3223    * on its location. Note that widget wrappers have a convenience getter
3224    * property (areaType) for this purpose.
3225    *
3226    * @param aArea the ID of the area whose type you want to know
3227    * @return TYPE_TOOLBAR or TYPE_MENU_PANEL depending on the area, null if
3228    *         the area is unknown.
3229    */
3230   getAreaType: function(aArea) {
3231     let area = gAreas.get(aArea);
3232     return area ? area.get("type") : null;
3233   },
3234   /**
3235    * Check if a toolbar is collapsed by default.
3236    *
3237    * @param aArea the ID of the area whose default-collapsed state you want to know.
3238    * @return `true` or `false` depending on the area, null if the area is unknown,
3239    *         or its collapsed state cannot normally be controlled by the user
3240    */
3241   isToolbarDefaultCollapsed: function(aArea) {
3242     let area = gAreas.get(aArea);
3243     return area ? area.get("defaultCollapsed") : null;
3244   },
3245   /**
3246    * Obtain the DOM node that is the customize target for an area in a
3247    * specific window.
3248    *
3249    * Areas can have a customization target that does not correspond to the
3250    * node itself. In particular, toolbars that have a customizationtarget
3251    * attribute set will have their customization target set to that node.
3252    * This means widgets will end up in the customization target, not in the
3253    * DOM node with the ID that corresponds to the area ID. This is useful
3254    * because it lets you have fixed content in a toolbar (e.g. the panel
3255    * menu item in the navbar) and have all the customizable widgets use
3256    * the customization target.
3257    *
3258    * Using this API yourself is discouraged; you should generally not need
3259    * to be asking for the DOM container node used for a particular area.
3260    * In particular, if you're wanting to check it in relation to a widget's
3261    * node, your DOM node might not be a direct child of the customize target
3262    * in a window if, for instance, the window is in customization mode, or if
3263    * this is an overflowable toolbar and the widget has been overflowed.
3264    *
3265    * @param aArea   the ID of the area whose customize target you want to have
3266    * @param aWindow the window where you want to fetch the DOM node.
3267    * @return the customize target DOM node for aArea in aWindow
3268    */
3269   getCustomizeTargetForArea: function(aArea, aWindow) {
3270     return CustomizableUIInternal.getCustomizeTargetForArea(aArea, aWindow);
3271   },
3272   /**
3273    * Reset the customization state back to its default.
3274    *
3275    * This is the nuclear option. You should never call this except if the user
3276    * explicitly requests it. Firefox does this when the user clicks the
3277    * "Restore Defaults" button in customize mode.
3278    */
3279   reset: function() {
3280     CustomizableUIInternal.reset();
3281   },
3283   /**
3284    * Undo the previous reset, can only be called immediately after a reset.
3285    * @return a promise that will be resolved when the operation is complete.
3286    */
3287   undoReset: function() {
3288     CustomizableUIInternal.undoReset();
3289   },
3291   /**
3292    * Remove a custom toolbar added in a previous version of Firefox or using
3293    * an add-on. NB: only works on the customizable toolbars generated by
3294    * the toolbox itself. Intended for use from CustomizeMode, not by
3295    * other consumers.
3296    * @param aToolbarId the ID of the toolbar to remove
3297    */
3298   removeExtraToolbar: function(aToolbarId) {
3299     CustomizableUIInternal.removeExtraToolbar(aToolbarId);
3300   },
3302   /**
3303    * Can the last Restore Defaults operation be undone.
3304    *
3305    * @return A boolean stating whether an undo of the
3306    *         Restore Defaults can be performed.
3307    */
3308   get canUndoReset() {
3309     return gUIStateBeforeReset.uiCustomizationState != null ||
3310            gUIStateBeforeReset.drawInTitlebar != null ||
3311            gUIStateBeforeReset.deveditionTheme != null;
3312   },
3314   /**
3315    * Get the placement of a widget. This is by far the best way to obtain
3316    * information about what the state of your widget is. The internals of
3317    * this call are cheap (no DOM necessary) and you will know where the user
3318    * has put your widget.
3319    *
3320    * @param aWidgetId the ID of the widget whose placement you want to know
3321    * @return
3322    *   {
3323    *     area: "somearea", // The ID of the area where the widget is placed
3324    *     position: 42 // the index in the placements array corresponding to
3325    *                  // your widget.
3326    *   }
3327    *
3328    *   OR
3329    *
3330    *   null // if the widget is not placed anywhere (ie in the palette)
3331    */
3332   getPlacementOfWidget: function(aWidgetId) {
3333     return CustomizableUIInternal.getPlacementOfWidget(aWidgetId, true);
3334   },
3335   /**
3336    * Check if a widget can be removed from the area it's in.
3337    *
3338    * Note that if you're wanting to move the widget somewhere, you should
3339    * generally be checking canWidgetMoveToArea, because that will return
3340    * true if the widget is already in the area where you want to move it (!).
3341    *
3342    * NB: oh, also, this method might lie if the widget in question is a
3343    *     XUL-provided widget and there are no windows open, because it
3344    *     can obviously not check anything in this case. It will return
3345    *     true. You will be able to move the widget elsewhere. However,
3346    *     once the user reopens a window, the widget will move back to its
3347    *     'proper' area automagically.
3348    *
3349    * @param aWidgetId a widget ID or DOM node to check
3350    * @return true if the widget can be removed from its area,
3351    *          false otherwise.
3352    */
3353   isWidgetRemovable: function(aWidgetId) {
3354     return CustomizableUIInternal.isWidgetRemovable(aWidgetId);
3355   },
3356   /**
3357    * Check if a widget can be moved to a particular area. Like
3358    * isWidgetRemovable but better, because it'll return true if the widget
3359    * is already in the right area.
3360    *
3361    * @param aWidgetId the widget ID or DOM node you want to move somewhere
3362    * @param aArea     the area ID you want to move it to.
3363    * @return true if this is possible, false if it is not. The same caveats as
3364    *              for isWidgetRemovable apply, however, if no windows are open.
3365    */
3366   canWidgetMoveToArea: function(aWidgetId, aArea) {
3367     return CustomizableUIInternal.canWidgetMoveToArea(aWidgetId, aArea);
3368   },
3369   /**
3370    * Whether we're in a default state. Note that non-removable non-default
3371    * widgets and non-existing widgets are not taken into account in determining
3372    * whether we're in the default state.
3373    *
3374    * NB: this is a property with a getter. The getter is NOT cheap, because
3375    * it does smart things with non-removable non-default items, non-existent
3376    * items, and so forth. Please don't call unless necessary.
3377    */
3378   get inDefaultState() {
3379     return CustomizableUIInternal.inDefaultState;
3380   },
3382   /**
3383    * Set a toolbar's visibility state in all windows.
3384    * @param aToolbarId    the toolbar whose visibility should be adjusted
3385    * @param aIsVisible    whether the toolbar should be visible
3386    */
3387   setToolbarVisibility: function(aToolbarId, aIsVisible) {
3388     CustomizableUIInternal.setToolbarVisibility(aToolbarId, aIsVisible);
3389   },
3391   /**
3392    * Get a localized property off a (widget?) object.
3393    *
3394    * NB: this is unlikely to be useful unless you're in Firefox code, because
3395    *     this code uses the builtin widget stringbundle, and can't be told
3396    *     to use add-on-provided strings. It's mainly here as convenience for
3397    *     custom builtin widgets that build their own DOM but use the same
3398    *     stringbundle as the other builtin widgets.
3399    *
3400    * @param aWidget     the object whose property we should use to fetch a
3401    *                    localizable string;
3402    * @param aProp       the property on the object to use for the fetching;
3403    * @param aFormatArgs (optional) any extra arguments to use for a formatted
3404    *                    string;
3405    * @param aDef        (optional) the default to return if we don't find the
3406    *                    string in the stringbundle;
3407    *
3408    * @return the localized string, or aDef if the string isn't in the bundle.
3409    *         If no default is provided,
3410    *           if aProp exists on aWidget, we'll return that,
3411    *           otherwise we'll return the empty string
3412    *
3413    */
3414   getLocalizedProperty: function(aWidget, aProp, aFormatArgs, aDef) {
3415     return CustomizableUIInternal.getLocalizedProperty(aWidget, aProp,
3416       aFormatArgs, aDef);
3417   },
3418   /**
3419    * Utility function to detect, find and set a keyboard shortcut for a menuitem
3420    * or (toolbar)button.
3421    *
3422    * @param aShortcutNode the XUL node where the shortcut will be derived from;
3423    * @param aTargetNode   (optional) the XUL node on which the `shortcut`
3424    *                      attribute will be set. If NULL, the shortcut will be
3425    *                      set on aShortcutNode;
3426    */
3427   addShortcut: function(aShortcutNode, aTargetNode) {
3428     return CustomizableUIInternal.addShortcut(aShortcutNode, aTargetNode);
3429   },
3430   /**
3431    * Given a node, walk up to the first panel in its ancestor chain, and
3432    * close it.
3433    *
3434    * @param aNode a node whose panel should be closed;
3435    */
3436   hidePanelForNode: function(aNode) {
3437     CustomizableUIInternal.hidePanelForNode(aNode);
3438   },
3439   /**
3440    * Check if a widget is a "special" widget: a spring, spacer or separator.
3441    *
3442    * @param aWidgetId the widget ID to check.
3443    * @return true if the widget is 'special', false otherwise.
3444    */
3445   isSpecialWidget: function(aWidgetId) {
3446     return CustomizableUIInternal.isSpecialWidget(aWidgetId);
3447   },
3448   /**
3449    * Add listeners to a panel that will close it. For use from the menu panel
3450    * and overflowable toolbar implementations, unlikely to be useful for
3451    * consumers.
3452    *
3453    * @param aPanel the panel to which listeners should be attached.
3454    */
3455   addPanelCloseListeners: function(aPanel) {
3456     CustomizableUIInternal.addPanelCloseListeners(aPanel);
3457   },
3458   /**
3459    * Remove close listeners that have been added to a panel with
3460    * addPanelCloseListeners. For use from the menu panel and overflowable
3461    * toolbar implementations, unlikely to be useful for consumers.
3462    *
3463    * @param aPanel the panel from which listeners should be removed.
3464    */
3465   removePanelCloseListeners: function(aPanel) {
3466     CustomizableUIInternal.removePanelCloseListeners(aPanel);
3467   },
3468   /**
3469    * Notify listeners a widget is about to be dragged to an area. For use from
3470    * Customize Mode only, do not use otherwise.
3471    *
3472    * @param aWidgetId the ID of the widget that is being dragged to an area.
3473    * @param aArea     the ID of the area to which the widget is being dragged.
3474    */
3475   onWidgetDrag: function(aWidgetId, aArea) {
3476     CustomizableUIInternal.notifyListeners("onWidgetDrag", aWidgetId, aArea);
3477   },
3478   /**
3479    * Notify listeners that a window is entering customize mode. For use from
3480    * Customize Mode only, do not use otherwise.
3481    * @param aWindow the window entering customize mode
3482    */
3483   notifyStartCustomizing: function(aWindow) {
3484     CustomizableUIInternal.notifyListeners("onCustomizeStart", aWindow);
3485   },
3486   /**
3487    * Notify listeners that a window is exiting customize mode. For use from
3488    * Customize Mode only, do not use otherwise.
3489    * @param aWindow the window exiting customize mode
3490    */
3491   notifyEndCustomizing: function(aWindow) {
3492     CustomizableUIInternal.notifyListeners("onCustomizeEnd", aWindow);
3493   },
3495   /**
3496    * Notify toolbox(es) of a particular event. If you don't pass aWindow,
3497    * all toolboxes will be notified. For use from Customize Mode only,
3498    * do not use otherwise.
3499    * @param aEvent the name of the event to send.
3500    * @param aDetails optional, the details of the event.
3501    * @param aWindow optional, the window in which to send the event.
3502    */
3503   dispatchToolboxEvent: function(aEvent, aDetails={}, aWindow=null) {
3504     CustomizableUIInternal.dispatchToolboxEvent(aEvent, aDetails, aWindow);
3505   },
3507   /**
3508    * Check whether an area is overflowable.
3509    *
3510    * @param aAreaId the ID of an area to check for overflowable-ness
3511    * @return true if the area is overflowable, false otherwise.
3512    */
3513   isAreaOverflowable: function(aAreaId) {
3514     let area = gAreas.get(aAreaId);
3515     return area ? area.get("type") == this.TYPE_TOOLBAR && area.get("overflowable")
3516                 : false;
3517   },
3518   /**
3519    * Obtain a string indicating the place of an element. This is intended
3520    * for use from customize mode; You should generally use getPlacementOfWidget
3521    * instead, which is cheaper because it does not use the DOM.
3522    *
3523    * @param aElement the DOM node whose place we need to check
3524    * @return "toolbar" if the node is in a toolbar, "panel" if it is in the
3525    *         menu panel, "palette" if it is in the (visible!) customization
3526    *         palette, undefined otherwise.
3527    */
3528   getPlaceForItem: function(aElement) {
3529     let place;
3530     let node = aElement;
3531     while (node && !place) {
3532       if (node.localName == "toolbar")
3533         place = "toolbar";
3534       else if (node.id == CustomizableUI.AREA_PANEL)
3535         place = "panel";
3536       else if (node.id == "customization-palette")
3537         place = "palette";
3539       node = node.parentNode;
3540     }
3541     return place;
3542   },
3544   /**
3545    * Check if a toolbar is builtin or not.
3546    * @param aToolbarId the ID of the toolbar you want to check
3547    */
3548   isBuiltinToolbar: function(aToolbarId) {
3549     return CustomizableUIInternal._builtinToolbars.has(aToolbarId);
3550   },
3552 Object.freeze(this.CustomizableUI);
3553 Object.freeze(this.CustomizableUI.windows);
3556  * All external consumers of widgets are really interacting with these wrappers
3557  * which provide a common interface.
3558  */
3561  * WidgetGroupWrapper is the common interface for interacting with an entire
3562  * widget group - AKA, all instances of a widget across a series of windows.
3563  * This particular wrapper is only used for widgets created via the provider
3564  * API.
3565  */
3566 function WidgetGroupWrapper(aWidget) {
3567   this.isGroup = true;
3569   const kBareProps = ["id", "source", "type", "disabled", "label", "tooltiptext",
3570                       "showInPrivateBrowsing"];
3571   for (let prop of kBareProps) {
3572     let propertyName = prop;
3573     this.__defineGetter__(propertyName, function() aWidget[propertyName]);
3574   }
3576   this.__defineGetter__("provider", function() CustomizableUI.PROVIDER_API);
3578   this.__defineSetter__("disabled", function(aValue) {
3579     aValue = !!aValue;
3580     aWidget.disabled = aValue;
3581     for (let [,instance] of aWidget.instances) {
3582       instance.disabled = aValue;
3583     }
3584   });
3586   this.forWindow = function WidgetGroupWrapper_forWindow(aWindow) {
3587     let wrapperMap;
3588     if (!gSingleWrapperCache.has(aWindow)) {
3589       wrapperMap = new Map();
3590       gSingleWrapperCache.set(aWindow, wrapperMap);
3591     } else {
3592       wrapperMap = gSingleWrapperCache.get(aWindow);
3593     }
3594     if (wrapperMap.has(aWidget.id)) {
3595       return wrapperMap.get(aWidget.id);
3596     }
3598     let instance = aWidget.instances.get(aWindow.document);
3599     if (!instance &&
3600         (aWidget.showInPrivateBrowsing || !PrivateBrowsingUtils.isWindowPrivate(aWindow))) {
3601       instance = CustomizableUIInternal.buildWidget(aWindow.document,
3602                                                     aWidget);
3603     }
3605     let wrapper = new WidgetSingleWrapper(aWidget, instance);
3606     wrapperMap.set(aWidget.id, wrapper);
3607     return wrapper;
3608   };
3610   this.__defineGetter__("instances", function() {
3611     // Can't use gBuildWindows here because some areas load lazily:
3612     let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id);
3613     if (!placement) {
3614       return [];
3615     }
3616     let area = placement.area;
3617     let buildAreas = gBuildAreas.get(area);
3618     if (!buildAreas) {
3619       return [];
3620     }
3621     return [this.forWindow(node.ownerDocument.defaultView) for (node of buildAreas)];
3622   });
3624   this.__defineGetter__("areaType", function() {
3625     let areaProps = gAreas.get(aWidget.currentArea);
3626     return areaProps && areaProps.get("type");
3627   });
3629   Object.freeze(this);
3633  * A WidgetSingleWrapper is a wrapper around a single instance of a widget in
3634  * a particular window.
3635  */
3636 function WidgetSingleWrapper(aWidget, aNode) {
3637   this.isGroup = false;
3639   this.node = aNode;
3640   this.provider = CustomizableUI.PROVIDER_API;
3642   const kGlobalProps = ["id", "type"];
3643   for (let prop of kGlobalProps) {
3644     this[prop] = aWidget[prop];
3645   }
3647   const kNodeProps = ["label", "tooltiptext"];
3648   for (let prop of kNodeProps) {
3649     let propertyName = prop;
3650     // Look at the node for these, instead of the widget data, to ensure the
3651     // wrapper always reflects this live instance.
3652     this.__defineGetter__(propertyName,
3653                           function() aNode.getAttribute(propertyName));
3654   }
3656   this.__defineGetter__("disabled", function() aNode.disabled);
3657   this.__defineSetter__("disabled", function(aValue) {
3658     aNode.disabled = !!aValue;
3659   });
3661   this.__defineGetter__("anchor", function() {
3662     let anchorId;
3663     // First check for an anchor for the area:
3664     let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id);
3665     if (placement) {
3666       anchorId = gAreas.get(placement.area).get("anchor");
3667     }
3668     if (!anchorId) {
3669       anchorId = aNode.getAttribute("cui-anchorid");
3670     }
3672     return anchorId ? aNode.ownerDocument.getElementById(anchorId)
3673                     : aNode;
3674   });
3676   this.__defineGetter__("overflowed", function() {
3677     return aNode.getAttribute("overflowedItem") == "true";
3678   });
3680   Object.freeze(this);
3684  * XULWidgetGroupWrapper is the common interface for interacting with an entire
3685  * widget group - AKA, all instances of a widget across a series of windows.
3686  * This particular wrapper is only used for widgets created via the old-school
3687  * XUL method (overlays, or programmatically injecting toolbaritems, or other
3688  * such things).
3689  */
3690 //XXXunf Going to need to hook this up to some events to keep it all live.
3691 function XULWidgetGroupWrapper(aWidgetId) {
3692   this.isGroup = true;
3693   this.id = aWidgetId;
3694   this.type = "custom";
3695   this.provider = CustomizableUI.PROVIDER_XUL;
3697   this.forWindow = function XULWidgetGroupWrapper_forWindow(aWindow) {
3698     let wrapperMap;
3699     if (!gSingleWrapperCache.has(aWindow)) {
3700       wrapperMap = new Map();
3701       gSingleWrapperCache.set(aWindow, wrapperMap);
3702     } else {
3703       wrapperMap = gSingleWrapperCache.get(aWindow);
3704     }
3705     if (wrapperMap.has(aWidgetId)) {
3706       return wrapperMap.get(aWidgetId);
3707     }
3709     let instance = aWindow.document.getElementById(aWidgetId);
3710     if (!instance) {
3711       // Toolbar palettes aren't part of the document, so elements in there
3712       // won't be found via document.getElementById().
3713       instance = aWindow.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0];
3714     }
3716     let wrapper = new XULWidgetSingleWrapper(aWidgetId, instance, aWindow.document);
3717     wrapperMap.set(aWidgetId, wrapper);
3718     return wrapper;
3719   };
3721   this.__defineGetter__("areaType", function() {
3722     let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId);
3723     if (!placement) {
3724       return null;
3725     }
3727     let areaProps = gAreas.get(placement.area);
3728     return areaProps && areaProps.get("type");
3729   });
3731   this.__defineGetter__("instances", function() {
3732     return [this.forWindow(win) for ([win,] of gBuildWindows)];
3733   });
3735   Object.freeze(this);
3739  * A XULWidgetSingleWrapper is a wrapper around a single instance of a XUL 
3740  * widget in a particular window.
3741  */
3742 function XULWidgetSingleWrapper(aWidgetId, aNode, aDocument) {
3743   this.isGroup = false;
3745   this.id = aWidgetId;
3746   this.type = "custom";
3747   this.provider = CustomizableUI.PROVIDER_XUL;
3749   let weakDoc = Cu.getWeakReference(aDocument);
3750   // If we keep a strong ref, the weak ref will never die, so null it out:
3751   aDocument = null;
3753   this.__defineGetter__("node", function() {
3754     // If we've set this to null (further down), we're sure there's nothing to
3755     // be gotten here, so bail out early:
3756     if (!weakDoc) {
3757       return null;
3758     }
3759     if (aNode) {
3760       // Return the last known node if it's still in the DOM...
3761       if (aNode.ownerDocument.contains(aNode)) {
3762         return aNode;
3763       }
3764       // ... or the toolbox
3765       let toolbox = aNode.ownerDocument.defaultView.gNavToolbox;
3766       if (toolbox && toolbox.palette && aNode.parentNode == toolbox.palette) {
3767         return aNode;
3768       }
3769       // If it isn't, clear the cached value and fall through to the "slow" case:
3770       aNode = null;
3771     }
3773     let doc = weakDoc.get();
3774     if (doc) {
3775       // Store locally so we can cache the result:
3776       aNode = CustomizableUIInternal.findWidgetInWindow(aWidgetId, doc.defaultView);
3777       return aNode;
3778     }
3779     // The weakref to the document is dead, we're done here forever more:
3780     weakDoc = null;
3781     return null;
3782   });
3784   this.__defineGetter__("anchor", function() {
3785     let anchorId;
3786     // First check for an anchor for the area:
3787     let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId);
3788     if (placement) {
3789       anchorId = gAreas.get(placement.area).get("anchor");
3790     }
3792     let node = this.node;
3793     if (!anchorId && node) {
3794       anchorId = node.getAttribute("cui-anchorid");
3795     }
3797     return (anchorId && node) ? node.ownerDocument.getElementById(anchorId) : node;
3798   });
3800   this.__defineGetter__("overflowed", function() {
3801     let node = this.node;
3802     if (!node) {
3803       return false;
3804     }
3805     return node.getAttribute("overflowedItem") == "true";
3806   });
3808   Object.freeze(this);
3811 const LAZY_RESIZE_INTERVAL_MS = 200;
3812 const OVERFLOW_PANEL_HIDE_DELAY_MS = 500;
3814 function OverflowableToolbar(aToolbarNode) {
3815   this._toolbar = aToolbarNode;
3816   this._collapsed = new Map();
3817   this._enabled = true;
3819   this._toolbar.setAttribute("overflowable", "true");
3820   let doc = this._toolbar.ownerDocument;
3821   this._target = this._toolbar.customizationTarget;
3822   this._list = doc.getElementById(this._toolbar.getAttribute("overflowtarget"));
3823   this._list.toolbox = this._toolbar.toolbox;
3824   this._list.customizationTarget = this._list;
3826   let window = this._toolbar.ownerDocument.defaultView;
3827   if (window.gBrowserInit.delayedStartupFinished) {
3828     this.init();
3829   } else {
3830     Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
3831   }
3834 OverflowableToolbar.prototype = {
3835   initialized: false,
3836   _forceOnOverflow: false,
3838   observe: function(aSubject, aTopic, aData) {
3839     if (aTopic == "browser-delayed-startup-finished" &&
3840         aSubject == this._toolbar.ownerDocument.defaultView) {
3841       Services.obs.removeObserver(this, "browser-delayed-startup-finished");
3842       this.init();
3843     }
3844   },
3846   init: function() {
3847     let doc = this._toolbar.ownerDocument;
3848     let window = doc.defaultView;
3849     window.addEventListener("resize", this);
3850     window.gNavToolbox.addEventListener("customizationstarting", this);
3851     window.gNavToolbox.addEventListener("aftercustomization", this);
3853     let chevronId = this._toolbar.getAttribute("overflowbutton");
3854     this._chevron = doc.getElementById(chevronId);
3855     this._chevron.addEventListener("command", this);
3856     this._chevron.addEventListener("dragover", this);
3857     this._chevron.addEventListener("dragend", this);
3859     let panelId = this._toolbar.getAttribute("overflowpanel");
3860     this._panel = doc.getElementById(panelId);
3861     this._panel.addEventListener("popuphiding", this);
3862     CustomizableUIInternal.addPanelCloseListeners(this._panel);
3864     CustomizableUI.addListener(this);
3866     // The 'overflow' event may have been fired before init was called.
3867     if (this._toolbar.overflowedDuringConstruction) {
3868       this.onOverflow(this._toolbar.overflowedDuringConstruction);
3869       this._toolbar.overflowedDuringConstruction = null;
3870     }
3872     this.initialized = true;
3873   },
3875   uninit: function() {
3876     this._toolbar.removeEventListener("overflow", this._toolbar);
3877     this._toolbar.removeEventListener("underflow", this._toolbar);
3878     this._toolbar.removeAttribute("overflowable");
3880     if (!this.initialized) {
3881       Services.obs.removeObserver(this, "browser-delayed-startup-finished");
3882       return;
3883     }
3885     this._disable();
3887     let window = this._toolbar.ownerDocument.defaultView;
3888     window.removeEventListener("resize", this);
3889     window.gNavToolbox.removeEventListener("customizationstarting", this);
3890     window.gNavToolbox.removeEventListener("aftercustomization", this);
3891     this._chevron.removeEventListener("command", this);
3892     this._chevron.removeEventListener("dragover", this);
3893     this._chevron.removeEventListener("dragend", this);
3894     this._panel.removeEventListener("popuphiding", this);
3895     CustomizableUI.removeListener(this);
3896     CustomizableUIInternal.removePanelCloseListeners(this._panel);
3897   },
3899   handleEvent: function(aEvent) {
3900     switch(aEvent.type) {
3901       case "aftercustomization":
3902         this._enable();
3903         break;
3904       case "command":
3905         if (aEvent.target == this._chevron) {
3906           this._onClickChevron(aEvent);
3907         } else {
3908           this._panel.hidePopup();
3909         }
3910         break;
3911       case "customizationstarting":
3912         this._disable();
3913         break;
3914       case "dragover":
3915         this._showWithTimeout();
3916         break;
3917       case "dragend":
3918         this._panel.hidePopup();
3919         break;
3920       case "popuphiding":
3921         this._onPanelHiding(aEvent);
3922         break;
3923       case "resize":
3924         this._onResize(aEvent);
3925     }
3926   },
3928   show: function() {
3929     let deferred = Promise.defer();
3930     if (this._panel.state == "open") {
3931       deferred.resolve();
3932       return deferred.promise;
3933     }
3934     let doc = this._panel.ownerDocument;
3935     this._panel.hidden = false;
3936     let contextMenu = doc.getElementById(this._panel.getAttribute("context"));
3937     gELS.addSystemEventListener(contextMenu, 'command', this, true);
3938     let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon");
3939     this._panel.openPopup(anchor || this._chevron);
3940     this._chevron.open = true;
3942     let overflowableToolbarInstance = this;
3943     this._panel.addEventListener("popupshown", function onPopupShown(aEvent) {
3944       this.removeEventListener("popupshown", onPopupShown);
3945       this.addEventListener("dragover", overflowableToolbarInstance);
3946       this.addEventListener("dragend", overflowableToolbarInstance);
3947       deferred.resolve();
3948     });
3950     return deferred.promise;
3951   },
3953   _onClickChevron: function(aEvent) {
3954     if (this._chevron.open) {
3955       this._panel.hidePopup();
3956       this._chevron.open = false;
3957     } else {
3958       this.show();
3959     }
3960   },
3962   _onPanelHiding: function(aEvent) {
3963     this._chevron.open = false;
3964     this._panel.removeEventListener("dragover", this);
3965     this._panel.removeEventListener("dragend", this);
3966     let doc = aEvent.target.ownerDocument;
3967     let contextMenu = doc.getElementById(this._panel.getAttribute("context"));
3968     gELS.removeSystemEventListener(contextMenu, 'command', this, true);
3969   },
3971   onOverflow: function(aEvent) {
3972     if (!this._enabled ||
3973         (aEvent && aEvent.target != this._toolbar.customizationTarget))
3974       return;
3976     let child = this._target.lastChild;
3978     while (child && this._target.scrollLeftMax > 0) {
3979       let prevChild = child.previousSibling;
3981       if (child.getAttribute("overflows") != "false") {
3982         this._collapsed.set(child.id, this._target.clientWidth);
3983         child.setAttribute("overflowedItem", true);
3984         child.setAttribute("cui-anchorid", this._chevron.id);
3985         CustomizableUIInternal.notifyListeners("onWidgetOverflow", child, this._target);
3987         this._list.insertBefore(child, this._list.firstChild);
3988         if (!this._toolbar.hasAttribute("overflowing")) {
3989           CustomizableUI.addListener(this);
3990         }
3991         this._toolbar.setAttribute("overflowing", "true");
3992       }
3993       child = prevChild;
3994     };
3996     let win = this._target.ownerDocument.defaultView;
3997     win.UpdateUrlbarSearchSplitterState();
3998   },
4000   _onResize: function(aEvent) {
4001     if (!this._lazyResizeHandler) {
4002       this._lazyResizeHandler = new DeferredTask(this._onLazyResize.bind(this),
4003                                                  LAZY_RESIZE_INTERVAL_MS);
4004     }
4005     this._lazyResizeHandler.arm();
4006   },
4008   _moveItemsBackToTheirOrigin: function(shouldMoveAllItems) {
4009     let placements = gPlacements.get(this._toolbar.id);
4010     while (this._list.firstChild) {
4011       let child = this._list.firstChild;
4012       let minSize = this._collapsed.get(child.id);
4014       if (!shouldMoveAllItems &&
4015           minSize &&
4016           this._target.clientWidth <= minSize) {
4017         return;
4018       }
4020       this._collapsed.delete(child.id);
4021       let beforeNodeIndex = placements.indexOf(child.id) + 1;
4022       // If this is a skipintoolbarset item, meaning it doesn't occur in the placements list,
4023       // we're inserting it at the end. This will mean first-in, first-out (more or less)
4024       // leading to as little change in order as possible.
4025       if (beforeNodeIndex == 0) {
4026         beforeNodeIndex = placements.length;
4027       }
4028       let inserted = false;
4029       for (; beforeNodeIndex < placements.length; beforeNodeIndex++) {
4030         let beforeNode = this._target.getElementsByAttribute("id", placements[beforeNodeIndex])[0];
4031         if (beforeNode) {
4032           this._target.insertBefore(child, beforeNode);
4033           inserted = true;
4034           break;
4035         }
4036       }
4037       if (!inserted) {
4038         this._target.appendChild(child);
4039       }
4040       child.removeAttribute("cui-anchorid");
4041       child.removeAttribute("overflowedItem");
4042       CustomizableUIInternal.notifyListeners("onWidgetUnderflow", child, this._target);
4043     }
4045     let win = this._target.ownerDocument.defaultView;
4046     win.UpdateUrlbarSearchSplitterState();
4048     if (!this._collapsed.size) {
4049       this._toolbar.removeAttribute("overflowing");
4050       CustomizableUI.removeListener(this);
4051     }
4052   },
4054   _onLazyResize: function() {
4055     if (!this._enabled)
4056       return;
4058     if (this._target.scrollLeftMax > 0) {
4059       this.onOverflow();
4060     } else {
4061       this._moveItemsBackToTheirOrigin();
4062     }
4063   },
4065   _disable: function() {
4066     this._enabled = false;
4067     this._moveItemsBackToTheirOrigin(true);
4068     if (this._lazyResizeHandler) {
4069       this._lazyResizeHandler.disarm();
4070     }
4071   },
4073   _enable: function() {
4074     this._enabled = true;
4075     this.onOverflow();
4076   },
4078   onWidgetBeforeDOMChange: function(aNode, aNextNode, aContainer) {
4079     if (aContainer != this._target && aContainer != this._list) {
4080       return;
4081     }
4082     // When we (re)move an item, update all the items that come after it in the list
4083     // with the minsize *of the item before the to-be-removed node*. This way, we
4084     // ensure that we try to move items back as soon as that's possible.
4085     if (aNode.parentNode == this._list) {
4086       let updatedMinSize;
4087       if (aNode.previousSibling) {
4088         updatedMinSize = this._collapsed.get(aNode.previousSibling.id);
4089       } else {
4090         // Force (these) items to try to flow back into the bar:
4091         updatedMinSize = 1;
4092       }
4093       let nextItem = aNode.nextSibling;
4094       while (nextItem) {
4095         this._collapsed.set(nextItem.id, updatedMinSize);
4096         nextItem = nextItem.nextSibling;
4097       }
4098     }
4099   },
4101   onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer) {
4102     if (aContainer != this._target && aContainer != this._list) {
4103       return;
4104     }
4106     let nowInBar = aNode.parentNode == aContainer;
4107     let nowOverflowed = aNode.parentNode == this._list;
4108     let wasOverflowed = this._collapsed.has(aNode.id);
4110     // If this wasn't overflowed before...
4111     if (!wasOverflowed) {
4112       // ... but it is now, then we added to the overflow panel. Exciting stuff:
4113       if (nowOverflowed) {
4114         // NB: we're guaranteed that it has a previousSibling, because if it didn't,
4115         // we would have added it to the toolbar instead. See getOverflowedNextNode.
4116         let prevId = aNode.previousSibling.id;
4117         let minSize = this._collapsed.get(prevId);
4118         this._collapsed.set(aNode.id, minSize);
4119         aNode.setAttribute("cui-anchorid", this._chevron.id);
4120         aNode.setAttribute("overflowedItem", true);
4121         CustomizableUIInternal.notifyListeners("onWidgetOverflow", aNode, this._target);
4122       }
4123       // If it is not overflowed and not in the toolbar, and was not overflowed
4124       // either, it moved out of the toolbar. That means there's now space in there!
4125       // Let's try to move stuff back:
4126       else if (!nowInBar) {
4127         this._moveItemsBackToTheirOrigin(true);
4128       }
4129       // If it's in the toolbar now, then we don't care. An overflow event may
4130       // fire afterwards; that's ok!
4131     }
4132     // If it used to be overflowed...
4133     else {
4134       // ... and isn't anymore, let's remove our bookkeeping:
4135       if (!nowOverflowed) {
4136         this._collapsed.delete(aNode.id);
4137         aNode.removeAttribute("cui-anchorid");
4138         aNode.removeAttribute("overflowedItem");
4139         CustomizableUIInternal.notifyListeners("onWidgetUnderflow", aNode, this._target);
4141         if (!this._collapsed.size) {
4142           this._toolbar.removeAttribute("overflowing");
4143           CustomizableUI.removeListener(this);
4144         }
4145       }
4146       // but if it still is, it must have changed places. Bookkeep:
4147       else {
4148         if (aNode.previousSibling) {
4149           let prevId = aNode.previousSibling.id;
4150           let minSize = this._collapsed.get(prevId);
4151           this._collapsed.set(aNode.id, minSize);
4152         } else {
4153           // If it's now the first item in the overflow list,
4154           // maybe we can return it:
4155           this._moveItemsBackToTheirOrigin();
4156         }
4157       }
4158     }
4159   },
4161   findOverflowedInsertionPoints: function(aNode) {
4162     let newNodeCanOverflow = aNode.getAttribute("overflows") != "false";
4163     let areaId = this._toolbar.id;
4164     let placements = gPlacements.get(areaId);
4165     let nodeIndex = placements.indexOf(aNode.id);
4166     let nodeBeforeNewNodeIsOverflown = false;
4168     let loopIndex = -1;
4169     while (++loopIndex < placements.length) {
4170       let nextNodeId = placements[loopIndex];
4171       if (loopIndex > nodeIndex) {
4172         if (newNodeCanOverflow && this._collapsed.has(nextNodeId)) {
4173           let nextNode = this._list.getElementsByAttribute("id", nextNodeId).item(0);
4174           if (nextNode) {
4175             return [this._list, nextNode];
4176           }
4177         }
4178         if (!nodeBeforeNewNodeIsOverflown || !newNodeCanOverflow) {
4179           let nextNode = this._target.getElementsByAttribute("id", nextNodeId).item(0);
4180           if (nextNode) {
4181             return [this._target, nextNode];
4182           }
4183         }
4184       } else if (loopIndex < nodeIndex && this._collapsed.has(nextNodeId)) {
4185         nodeBeforeNewNodeIsOverflown = true;
4186       }
4187     }
4189     let containerForAppending = (this._collapsed.size && newNodeCanOverflow) ?
4190                                 this._list : this._target;
4191     return [containerForAppending, null];
4192   },
4194   getContainerFor: function(aNode) {
4195     if (aNode.getAttribute("overflowedItem") == "true") {
4196       return this._list;
4197     }
4198     return this._target;
4199   },
4201   _hideTimeoutId: null,
4202   _showWithTimeout: function() {
4203     this.show().then(function () {
4204       let window = this._toolbar.ownerDocument.defaultView;
4205       if (this._hideTimeoutId) {
4206         window.clearTimeout(this._hideTimeoutId);
4207       }
4208       this._hideTimeoutId = window.setTimeout(() => {
4209         if (!this._panel.firstChild.matches(":hover")) {
4210           this._panel.hidePopup();
4211         }
4212       }, OVERFLOW_PANEL_HIDE_DELAY_MS);
4213     }.bind(this));
4214   },
4217 CustomizableUIInternal.initialize();