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/. */
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);
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";
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.
48 const kSubviewEvents = [
54 * The method name to use for ES6 iteration. If Symbols are enabled in
55 * this build, use Symbol.iterator; otherwise "@@iterator".
57 const JS_HAS_SYMBOLS = typeof Symbol === "function";
58 const kIteratorSymbol = JS_HAS_SYMBOLS ? Symbol.iterator : "@@iterator";
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)
67 * gPalette is a map of every widget that CustomizableUI.jsm knows about, keyed
70 let gPalette = new Map();
73 * gAreas maps area IDs to Sets of properties about those areas. An area is a
74 * place where a widget can be put.
76 let gAreas = new Map();
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).
83 let gPlacements = new Map();
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
90 let gFuturePlacements = new Map();
92 //XXXunf Temporary. Need a nice way to abstract functions to build widgets
94 let gSupportedWidgetTypes = new Set(["button", "view", "custom"]);
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.
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
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
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
123 let gPendingBuildAreas = new Map();
125 let gSavedState = null;
126 let gRestoring = 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.
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.
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]";
157 let CustomizableUIInternal = {
158 initialize: function() {
161 this.addListener(this);
162 this._defineBuiltInWidgets();
163 this.loadSavedState();
164 this._introduceNewBuiltinWidgets();
166 let panelPlacements = [
170 "privatebrowsing-button",
176 "preferences-button",
178 #ifndef MOZ_DEV_EDITION
183 if (gPalette.has("switch-to-metro-button")) {
184 panelPlacements.push("switch-to-metro-button");
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");
196 let showCharacterEncoding = Services.prefs.getComplexValue(
197 "browser.menu.showCharacterEncoding",
198 Ci.nsIPrefLocalizedString
200 if (showCharacterEncoding == "true") {
201 panelPlacements.push("characterencoding-button");
204 this.registerArea(CustomizableUI.AREA_PANEL, {
205 anchor: "PanelUI-menu-button",
206 type: CustomizableUI.TYPE_MENU_PANEL,
207 defaultPlacements: panelPlacements
209 PanelWideWidgetTracker.init();
211 let navbarPlacements = [
214 #ifdef MOZ_DEV_EDITION
217 "bookmarks-menu-button",
223 if (Services.prefs.getBoolPref(kPrefWebIDEInNavbar)) {
224 navbarPlacements.push("webide-button");
227 this.registerArea(CustomizableUI.AREA_NAVBAR, {
229 type: CustomizableUI.TYPE_TOOLBAR,
231 defaultPlacements: navbarPlacements,
232 defaultCollapsed: false,
235 this.registerArea(CustomizableUI.AREA_MENUBAR, {
237 type: CustomizableUI.TYPE_TOOLBAR,
241 get defaultCollapsed() {
242 #ifdef MENUBAR_CAN_AUTOHIDE
243 #if defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_QT)
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";
256 this.registerArea(CustomizableUI.AREA_TABSTRIP, {
258 type: CustomizableUI.TYPE_TOOLBAR,
264 defaultCollapsed: null,
266 this.registerArea(CustomizableUI.AREA_BOOKMARKS, {
268 type: CustomizableUI.TYPE_TOOLBAR,
270 "personal-bookmarks",
272 defaultCollapsed: true,
275 this.registerArea(CustomizableUI.AREA_ADDONBAR, {
276 type: CustomizableUI.TYPE_TOOLBAR,
278 defaultPlacements: ["addonbar-closebutton", "status-bar"],
279 defaultCollapsed: false,
283 get _builtinToolbars() {
285 CustomizableUI.AREA_NAVBAR,
286 CustomizableUI.AREA_BOOKMARKS,
287 CustomizableUI.AREA_TABSTRIP,
288 CustomizableUI.AREA_ADDONBAR,
290 CustomizableUI.AREA_MENUBAR,
295 _defineBuiltInWidgets: function() {
296 for (let widgetDefinition of CustomizableWidgets) {
297 this.createBuiltinWidget(widgetDefinition);
301 _introduceNewBuiltinWidgets: function() {
302 if (!gSavedState || gSavedState.currentVersion >= kVersion) {
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);
314 gFuturePlacements.set(widget.defaultArea, new Set([id]));
319 if (currentVersion < 2) {
320 // Nuke the old 'loop-call-button' out of orbit.
321 CustomizableUI.removeWidgetFromArea("loop-call-button");
324 if (currentVersion < 4) {
325 CustomizableUI.removeWidgetFromArea("loop-button-throttled");
329 wrapWidget: function(aWidgetId) {
330 if (gGroupWrapperCache.has(aWidgetId)) {
331 return gGroupWrapperCache.get(aWidgetId);
334 let provider = this.getWidgetProvider(aWidgetId);
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);
345 return widget.wrapper;
348 // PROVIDER_SPECIAL gets treated the same as PROVIDER_XUL.
349 let wrapper = new XULWidgetGroupWrapper(aWidgetId);
350 gGroupWrapperCache.set(aWidgetId, wrapper);
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");
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 + "'");
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 ));
372 props.set(key, aProperties[key]);
375 // Default to a toolbar:
376 if (!props.has("type")) {
377 props.set("type", CustomizableUI.TYPE_TOOLBAR);
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.")
385 if (!props.has("defaultCollapsed")) {
386 props.set("defaultCollapsed", true);
388 } else if (props.has("defaultCollapsed")) {
389 throw new Error("defaultCollapsed only applies for TYPE_TOOLBAR areas.");
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"));
397 // And to no placements:
398 if (!props.has("defaultPlacements")) {
399 props.set("defaultPlacements", []);
401 // Sanity check default placements array:
402 if (!Array.isArray(props.get("defaultPlacements"))) {
403 throw new Error("Should provide an array of default placements");
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());
416 this.restoreStateForArea(aName);
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);
425 gPendingBuildAreas.delete(aName);
430 unregisterArea: function(aName, aDestroyPlacements) {
431 if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
432 throw new Error("Invalid area name");
434 if (!gAreas.has(aName) && !gPlacements.has(aName)) {
435 throw new Error("Area not registered");
438 // Move all the widgets out
439 this.beginBatchUpdate();
441 let placements = gPlacements.get(aName);
443 // Need to clone this array so removeWidgetFromArea doesn't modify it
444 placements = [...placements];
445 placements.forEach(this.removeWidgetFromArea, this);
448 // Delete all remaining traces.
449 gAreas.delete(aName);
450 // Only destroy placements when necessary:
451 if (aDestroyPlacements) {
452 gPlacements.delete(aName);
454 // Otherwise we need to re-set them, as removeFromArea will have emptied
456 gPlacements.set(aName, placements);
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);
466 gBuildAreas.delete(aName);
468 this.endBatchUpdate(true);
472 registerToolbarNode: function(aToolbar, aExistingChildren) {
473 let area = aToolbar.id;
474 if (gBuildAreas.has(area) && gBuildAreas.get(area).has(aToolbar)) {
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());
489 let pendingNodes = gPendingBuildAreas.get(area);
490 pendingNodes.set(aToolbar, aExistingChildren);
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);
500 this.beginBatchUpdate();
502 let placements = gPlacements.get(area);
503 if (!placements && areaProperties.has("legacy")) {
504 let legacyState = aToolbar.getAttribute("currentset");
506 legacyState = legacyState.split(",").filter(s => s);
509 // Manually restore the state here, so the legacy state can be converted.
510 this.restoreStateForArea(area, legacyState);
511 placements = gPlacements.get(area);
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);
521 if (areaProperties.has("overflowable")) {
522 aToolbar.overflowable = new OverflowableToolbar(aToolbar);
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.
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);
538 this.notifyListeners("onAreaNodeRegistered", area, aToolbar.customizationTarget);
539 aToolbar.setAttribute("currentset", placements.join(","));
541 this.endBatchUpdate();
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;
553 throw new Error("Expected area " + aArea
554 + " to have a customizationTarget attribute.");
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;
563 this.beginBatchUpdate();
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;
573 if (currentNode && currentNode.id == id) {
574 currentNode = currentNode.nextSibling;
578 if (this.isSpecialWidget(id) && areaIsPanel) {
579 placementsToRemove.add(id);
583 let [provider, node] = this.getWidgetNode(id, window);
585 LOG("Unknown widget: " + id);
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);
597 } else if (provider == CustomizableUI.PROVIDER_XUL &&
598 node.parentNode != container && !this.isWidgetRemovable(node)) {
599 placementsToRemove.add(id);
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) {
610 this.ensureButtonContextMenu(node, aAreaNode);
611 if (node.localName == "toolbarbutton") {
613 node.setAttribute("wrap", "true");
615 node.removeAttribute("wrap");
619 this.insertWidgetBefore(node, currentNode, container, aArea);
621 this.notifyListeners("onWidgetReset", node, container);
622 } else if (gUndoResetting) {
623 this.notifyListeners("onWidgetUndoMove", node, container);
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);
646 container.removeChild(node);
649 node.setAttribute("removable", false);
650 LOG("Adding non-removable widget to placements of " + aArea + ": " +
652 gPlacements.get(aArea).push(node.id);
656 node = previousSibling;
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);
672 this.notifyListeners("onAreaReset", aArea, container);
675 this.endBatchUpdate();
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());
686 gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel));
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);
695 panels.delete(this._getPanelForNode(aPanel));
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 :
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");
717 getWidgetProvider: function(aWidgetId) {
718 if (this.isSpecialWidget(aWidgetId)) {
719 return CustomizableUI.PROVIDER_SPECIAL;
721 if (gPalette.has(aWidgetId)) {
722 return CustomizableUI.PROVIDER_API;
724 // If this was an API widget that was destroyed, return null:
725 if (gSeenWidgets.has(aWidgetId)) {
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;
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];
746 let widget = gPalette.get(aWidgetId);
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) ];
756 return [ CustomizableUI.PROVIDER_API,
757 this.buildWidget(document, widget) ];
760 LOG("Searching for " + aWidgetId + " in toolbox.");
761 let node = this.findWidgetInWindow(aWidgetId, aWindow);
763 return [ CustomizableUI.PROVIDER_XUL, node ];
766 LOG("No node for " + aWidgetId + " found.");
770 registerMenuPanel: function(aPanelContents) {
771 if (gBuildAreas.has(CustomizableUI.AREA_PANEL) &&
772 gBuildAreas.get(CustomizableUI.AREA_PANEL).has(aPanelContents)) {
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);
794 this.ensureButtonContextMenu(child, aPanelContents);
795 child.setAttribute("wrap", "true");
798 this.registerBuildArea(CustomizableUI.AREA_PANEL, aPanelContents);
801 onWidgetAdded: function(aWidgetId, aArea, aPosition) {
802 this.insertNode(aWidgetId, aArea, aPosition, true);
805 this._clearPreviousUIState();
809 onWidgetRemoved: function(aWidgetId, aArea) {
810 let areaNodes = gBuildAreas.get(aArea);
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
822 for (let areaNode of areaNodes) {
823 let window = areaNode.ownerDocument.defaultView;
824 if (!showInPrivateBrowsing &&
825 PrivateBrowsingUtils.isWindowPrivate(window)) {
829 let container = areaNode.customizationTarget;
830 let widgetNode = window.document.getElementById(aWidgetId);
831 if (widgetNode && isOverflowable) {
832 container = areaNode.overflowable.getContainerFor(widgetNode);
835 if (!widgetNode || !container.contains(widgetNode)) {
836 INFO("Widget " + aWidgetId + " not found, unable to remove from " + aArea);
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);
851 areaNode.toolbox.palette.appendChild(widgetNode);
853 this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null, container, true);
856 areaNode.setAttribute("currentset", gPlacements.get(aArea).join(','));
859 let windowCache = gSingleWrapperCache.get(window);
861 windowCache.delete(aWidgetId);
865 this._clearPreviousUIState();
869 onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) {
870 this.insertNode(aWidgetId, aArea, aNewPosition);
872 this._clearPreviousUIState();
876 onCustomizeEnd: function(aWindow) {
877 this._clearPreviousUIState();
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;
887 this.registerBuildWindow(window);
889 // Also register this build area's toolbox.
891 gBuildWindows.get(window).add(aNode.toolbox);
894 if (!gBuildAreas.has(aArea)) {
895 gBuildAreas.set(aArea, new Set());
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");
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);
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;
934 areaNodes.delete(node);
939 for (let [,widget] of gPalette) {
940 widget.instances.delete(document);
941 this.notifyListeners("onWidgetInstanceRemoved", widget.id, document);
944 for (let [area, areaMap] of gPendingBuildAreas) {
946 for (let [areaNode, ] of areaMap) {
947 if (areaNode.ownerDocument == document) {
948 toDelete.push(areaNode);
951 for (let areaNode of toDelete) {
952 areaMap.delete(toDelete);
956 this.notifyListeners("onWindowClosed", aWindow);
959 setLocationAttributes: function(aNode, aArea) {
960 let props = gAreas.get(aArea);
962 throw new Error("Expected area " + aArea + " to have a properties Map " +
963 "associated with it.");
966 aNode.setAttribute("cui-areatype", props.get("type") || "");
967 let anchor = props.get("anchor");
969 aNode.setAttribute("cui-anchorid", anchor);
971 aNode.removeAttribute("cui-anchorid");
975 removeLocationAttributes: function(aNode) {
976 aNode.removeAttribute("cui-areatype");
977 aNode.removeAttribute("cui-anchorid");
980 insertNode: function(aWidgetId, aArea, aPosition, isNew) {
981 let areaNodes = gBuildAreas.get(aArea);
986 let placements = gPlacements.get(aArea);
988 ERROR("Could not find any placements for " + aArea +
989 " when moving a widget.");
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);
1000 insertNodeInWindow: function(aWidgetId, aAreaNode, isNew) {
1001 let window = aAreaNode.ownerDocument.defaultView;
1002 let showInPrivateBrowsing = gPalette.has(aWidgetId)
1003 ? gPalette.get(aWidgetId).showInPrivateBrowsing
1006 if (!showInPrivateBrowsing && PrivateBrowsingUtils.isWindowPrivate(window)) {
1010 let [, widgetNode] = this.getWidgetNode(aWidgetId, window);
1012 ERROR("Widget '" + aWidgetId + "' not found, unable to move");
1016 let areaId = aAreaNode.id;
1018 this.ensureButtonContextMenu(widgetNode, aAreaNode);
1019 if (widgetNode.localName == "toolbarbutton" && areaId == CustomizableUI.AREA_PANEL) {
1020 widgetNode.setAttribute("wrap", "true");
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(','));
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);
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);
1050 return [container, nextNode];
1054 return [container, null];
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);
1064 handleEvent: function(aEvent) {
1065 switch (aEvent.type) {
1067 if (!this._originalEventInPanel(aEvent)) {
1070 aEvent = aEvent.sourceEvent;
1074 this.maybeAutoHidePanel(aEvent);
1077 this.unregisterBuildWindow(aEvent.currentTarget);
1082 _originalEventInPanel: function(aEvent) {
1083 let e = aEvent.sourceEvent;
1087 let node = this._getPanelForNode(e.target);
1092 let panels = gPanelsForWindow.get(win);
1093 return !!panels && panels.has(node);
1096 isSpecialWidget: function(aId) {
1097 return (aId.startsWith(kSpecialWidgetPfx) ||
1098 aId.startsWith("separator") ||
1099 aId.startsWith("spring") ||
1100 aId.startsWith("spacer"));
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);
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") {
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.
1126 findWidgetInWindow: function(aId, aWindow) {
1127 if (!gBuildWindows.has(aWindow)) {
1128 throw new Error("Build window not registered");
1132 ERROR("findWidgetInWindow was passed an empty string.");
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);
1142 let parent = node.parentNode;
1143 while (parent && !(parent.customizationTarget ||
1144 parent == aWindow.gNavToolbox.palette)) {
1145 parent = parent.parentNode;
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);
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
1173 let node = toolbox.palette.getElementsByAttribute("id", aId)[0];
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);
1188 buildWidget: function(aDocument, aWidget) {
1189 if (typeof aWidget == "string") {
1190 aWidget = gPalette.get(aWidget);
1193 throw new Error("buildWidget was passed a non-widget to build.");
1196 LOG("Building " + aWidget.id + " of type " + aWidget.type);
1199 if (aWidget.type == "custom") {
1200 if (aWidget.onBuild) {
1201 node = aWidget.onBuild(aDocument);
1203 if (!node || !(node instanceof aDocument.defaultView.XULElement))
1204 ERROR("Custom widget with id " + aWidget.id + " does not return a valid node");
1207 if (aWidget.onBeforeCreated) {
1208 aWidget.onBeforeCreated(aDocument);
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);
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);
1225 additionalTooltipArguments.push(ShortcutUtils.prettifyShortcut(keyEl));
1227 ERROR("Key element with id '" + aWidget.shortcutId + "' for widget '" + aWidget.id +
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);
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);
1259 LOG("Widget " + aWidget.id + " showing and hiding event handlers set.");
1261 ERROR("Could not find the view node with id: " + aWidget.viewId +
1262 ", for widget: " + aWidget.id + ".");
1266 if (aWidget.onCreated) {
1267 aWidget.onCreated(node);
1271 aWidget.instances.set(aDocument, node);
1275 getLocalizedProperty: function(aWidget, aProp, aFormatArgs, aDef) {
1276 if (typeof aWidget == "string") {
1277 aWidget = gPalette.get(aWidget);
1280 throw new Error("getLocalizedProperty was passed a non-widget to work with.");
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
1293 name = aWidget.id + "." + aProp;
1297 if (Array.isArray(aFormatArgs) && aFormatArgs.length) {
1298 return gWidgetsBundle.formatStringFromName(name, aFormatArgs,
1299 aFormatArgs.length) || def;
1301 return gWidgetsBundle.GetStringFromName(name) || def;
1304 ERROR("Could not localize property '" + name + "'.");
1310 addShortcut: function(aShortcutNode, aTargetNode) {
1312 aTargetNode = aShortcutNode;
1313 let document = aShortcutNode.ownerDocument;
1315 // Detect if we've already been here before.
1316 if (!aTargetNode || aTargetNode.hasAttribute("shortcut"))
1319 let shortcutId = aShortcutNode.getAttribute("key");
1322 shortcut = document.getElementById(shortcutId);
1324 let commandId = aShortcutNode.getAttribute("command");
1326 shortcut = ShortcutUtils.findShortcut(document.getElementById(commandId));
1332 aTargetNode.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(shortcut));
1335 handleWidgetCommand: function(aWidget, aNode, aEvent) {
1336 LOG("handleWidgetCommand");
1338 if (aWidget.type == "button") {
1339 if (aWidget.onCommand) {
1341 aWidget.onCommand.call(null, aEvent);
1346 //XXXunf Need to think this through more, and formalize.
1347 Services.obs.notifyObservers(aNode,
1348 "customizedui-widget-command",
1351 } else if (aWidget.type == "view") {
1352 let ownerWindow = aNode.ownerDocument.defaultView;
1353 let area = this.getPlacementOfWidget(aNode.id).area;
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;
1362 ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, area);
1366 handleWidgetClick: function(aWidget, aNode, aEvent) {
1367 LOG("handleWidgetClick");
1368 if (aWidget.onClick) {
1370 aWidget.onClick.call(null, aEvent);
1375 //XXXunf Need to think this through more, and formalize.
1376 Services.obs.notifyObservers(aNode, "customizedui-widget-click", aWidget.id);
1380 _getPanelForNode: function(aNode) {
1382 while (panel && panel.localName != "panel")
1383 panel = panel.parentNode;
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
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;
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.
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
1416 // whether we're in a toolbarbutton/toolbaritem
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";
1438 if (isMenuItem && target.hasAttribute("closemenu")) {
1439 let closemenuVal = target.getAttribute("closemenu");
1440 menuitemCloseMenu = (closemenuVal == "single" || closemenuVal == "none") ?
1441 closemenuVal : "auto";
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") {
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) {
1454 // We need specific code for popups: the item on which they were invoked
1455 // isn't necessarily in their parentNode chain:
1457 let topmostMenuPopup = getMenuPopupForDescendant(target);
1458 target = (topmostMenuPopup && topmostMenuPopup.triggerNode) ||
1461 target = target.parentNode;
1465 // If the user clicked a menu item...
1467 // We care if we're in an input also,
1468 // or if the user specified closemenu!="auto":
1469 if (inInput || menuitemCloseMenu != "auto") {
1472 // Otherwise, we're probably fine to close the panel
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") {
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";
1487 // otherwise, this is the outer button, and the user will now
1488 // interact with the menu:
1491 return inInput || !inItem;
1494 hidePanelForNode: function(aNode) {
1495 let panel = this._getPanelForNode(aNode);
1501 maybeAutoHidePanel: function(aEvent) {
1502 if (aEvent.type == "keypress") {
1503 if (aEvent.keyCode != aEvent.DOM_VK_RETURN) {
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
1511 } else if (aEvent.type != "command") { // mouse events:
1512 if (aEvent.defaultPrevented || aEvent.button != 0) {
1515 let isInteractive = this._isOnInteractiveElement(aEvent);
1516 LOG("maybeAutoHidePanel: interactive ? " + isInteractive);
1517 if (isInteractive) {
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") {
1537 target = target.parentNode;
1539 if (closemenu == "none" || widgetType == "view") {
1543 if (closemenu == "single") {
1544 let panel = this._getPanelForNode(target);
1545 let multiview = panel.querySelector("panelmultiview");
1546 if (multiview.showingSubView) {
1547 multiview.showMainView();
1552 // If we get here, we can actually hide the popup:
1553 this.hidePanelForNode(aEvent.target);
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
1568 for (let [id, widget] of gPalette) {
1569 if (!widget.currentArea) {
1570 if (widget.showInPrivateBrowsing || !isWindowPrivate) {
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);
1584 return [...widgets];
1587 getPlacementOfWidget: function(aWidgetId, aOnlyRegistered, aDeadAreas) {
1588 if (aOnlyRegistered && !this.widgetExists(aWidgetId)) {
1592 for (let [area, placements] of gPlacements) {
1593 if (!gAreas.has(area) && !aDeadAreas) {
1596 let index = placements.indexOf(aWidgetId);
1598 return { area: area, position: index };
1605 widgetExists: function(aWidgetId) {
1606 if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) {
1610 // Destroyed API widgets are in gSeenWidgets, but not in gPalette:
1611 if (gSeenWidgets.has(aWidgetId)) {
1615 // We're assuming XUL widgets always exist, as it's much harder to check,
1616 // and checking would be much more error prone.
1620 addWidgetToArea: function(aWidgetId, aArea, aPosition, aInitialAdd) {
1621 if (!gAreas.has(aArea)) {
1622 throw new Error("Unknown customization area: " + aArea);
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)) {
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);
1643 if (this.isSpecialWidget(aWidgetId)) {
1644 aWidgetId = this.ensureSpecialWidgetId(aWidgetId);
1647 let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
1648 if (oldPlacement && oldPlacement.area == aArea) {
1649 this.moveWidgetWithinArea(aWidgetId, aPosition);
1653 // Do nothing if the widget is not allowed to move to the target area.
1654 if (!this.canWidgetMoveToArea(aWidgetId, aArea)) {
1659 this.removeWidgetFromArea(aWidgetId);
1662 if (!gPlacements.has(aArea)) {
1663 gPlacements.set(aArea, [aWidgetId]);
1666 let placements = gPlacements.get(aArea);
1667 if (typeof aPosition != "number") {
1668 aPosition = placements.length;
1670 if (aPosition < 0) {
1673 placements.splice(aPosition, 0, aWidgetId);
1676 let widget = gPalette.get(aWidgetId);
1678 widget.currentArea = aArea;
1679 widget.currentPosition = aPosition;
1682 // We initially set placements with addWidgetToArea, so in that case
1683 // we don't consider the area "dirtied".
1685 gDirtyAreaCache.add(aArea);
1691 this.notifyListeners("onWidgetAdded", aWidgetId, aArea, aPosition);
1694 removeWidgetFromArea: function(aWidgetId) {
1695 let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
1696 if (!oldPlacement) {
1700 if (!this.isWidgetRemovable(aWidgetId)) {
1704 let placements = gPlacements.get(oldPlacement.area);
1705 let position = placements.indexOf(aWidgetId);
1706 if (position != -1) {
1707 placements.splice(position, 1);
1710 let widget = gPalette.get(aWidgetId);
1712 widget.currentArea = null;
1713 widget.currentPosition = null;
1718 gDirtyAreaCache.add(oldPlacement.area);
1720 this.notifyListeners("onWidgetRemoved", aWidgetId, oldPlacement.area);
1723 moveWidgetWithinArea: function(aWidgetId, aPosition) {
1724 let oldPlacement = this.getPlacementOfWidget(aWidgetId);
1725 if (!oldPlacement) {
1729 let placements = gPlacements.get(oldPlacement.area);
1730 if (typeof aPosition != "number") {
1731 aPosition = placements.length;
1732 } else if (aPosition < 0) {
1734 } else if (aPosition > placements.length) {
1735 aPosition = placements.length;
1738 let widget = gPalette.get(aWidgetId);
1740 widget.currentPosition = aPosition;
1741 widget.currentArea = oldPlacement.area;
1744 if (aPosition == oldPlacement.position) {
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) {
1754 placements.splice(aPosition, 0, aWidgetId);
1757 gDirtyAreaCache.add(oldPlacement.area);
1761 this.notifyListeners("onWidgetMoved", aWidgetId, oldPlacement.area,
1762 oldPlacement.position, aPosition);
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() {
1775 state = Services.prefs.getCharPref(kPrefCustomizationState);
1777 LOG("No saved state found");
1778 // This will fail if nothing has been customized, so silently fall back to
1786 gSavedState = JSON.parse(state);
1787 if (typeof gSavedState != "object" || gSavedState === null) {
1788 throw "Invalid saved state";
1791 Services.prefs.clearUserPref(kPrefCustomizationState);
1793 LOG("Error loading saved UI customization state, falling back to defaults.");
1796 if (!("placements" in gSavedState)) {
1797 gSavedState.placements = {};
1800 if (!("currentVersion" in gSavedState)) {
1801 gSavedState.currentVersion = 0;
1804 gSeenWidgets = new Set(gSavedState.seen || []);
1805 gDirtyAreaCache = new Set(gSavedState.dirtyAreaCache || []);
1806 gNewElementCount = gSavedState.newElementCount || 0;
1809 restoreStateForArea: function(aArea, aLegacyState) {
1810 let placementsPreexisted = gPlacements.has(aArea);
1812 this.beginBatchUpdate();
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);
1825 gPlacements.set(aArea, []);
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);
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.
1847 LOG("Restoring " + aArea + " from default state");
1848 let defaults = gAreas.get(aArea).get("defaultPlacements");
1850 for (let id of defaults)
1851 this.addWidgetToArea(id, aArea, null, true);
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);
1865 LOG("Placements for " + aArea + ":\n\t" + gPlacements.get(aArea).join("\n\t"));
1869 this.endBatchUpdate();
1873 saveState: function() {
1874 if (gInBatchStack || !gDirty) {
1877 // Clone because we want to modify this map:
1878 let state = { placements: new Map(gPlacements),
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);
1896 LOG("Saving state.");
1897 let serialized = JSON.stringify(state, this.serializerHelper);
1898 LOG("State saved as: " + serialized);
1899 Services.prefs.setCharPref(kPrefCustomizationState, serialized);
1903 serializerHelper: function(aKey, aValue) {
1904 if (typeof aValue == "object" && aValue.constructor.name == "Map") {
1906 for (let [mapKey, mapValue] of aValue)
1907 result[mapKey] = mapValue;
1911 if (typeof aValue == "object" && aValue.constructor.name == "Set") {
1918 beginBatchUpdate: function() {
1922 endBatchUpdate: function(aForceDirty) {
1924 if (aForceDirty === true) {
1927 if (gInBatchStack == 0) {
1929 } else if (gInBatchStack < 0) {
1930 throw new Error("The batch editing stack should never reach a negative number.");
1934 addListener: function(aListener) {
1935 gListeners.add(aListener);
1938 removeListener: function(aListener) {
1939 if (aListener == this) {
1943 gListeners.delete(aListener);
1946 notifyListeners: function(aEvent, ...aArgs) {
1951 for (let listener of gListeners) {
1953 if (typeof listener[aEvent] == "function") {
1954 listener[aEvent].apply(listener, aArgs);
1957 ERROR(e + " -- " + e.fileName + ":" + e.lineNumber);
1962 _dispatchToolboxEventToWindow: function(aEventType, aDetails, aWindow) {
1963 let evt = new aWindow.CustomEvent(aEventType, {
1968 aWindow.gNavToolbox.dispatchEvent(evt);
1971 dispatchToolboxEvent: function(aEventType, aDetails={}, aWindow=null) {
1973 return this._dispatchToolboxEventToWindow(aEventType, aDetails, aWindow);
1975 for (let [win, ] of gBuildWindows) {
1976 this._dispatchToolboxEventToWindow(aEventType, aDetails, win);
1980 createWidget: function(aProperties) {
1981 let widget = this.normalizeWidget(aProperties, CustomizableUI.SOURCE_EXTERNAL);
1982 //XXXunf This should probably throw.
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);
1994 cache.delete(widget.id);
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;
2008 if (addToDefaultPlacements) {
2009 if (area.has("defaultPlacements")) {
2010 area.get("defaultPlacements").push(widget.id);
2012 area.set("defaultPlacements", [widget.id]);
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);
2025 widgetMightNeedAutoAdding = false;
2026 if (areaIsRegistered) {
2027 widget.currentArea = area;
2028 widget.currentPosition = index;
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
2037 if (widgetMightNeedAutoAdding && gSavedState) {
2038 for (let area of Object.keys(gSavedState.placements)) {
2039 if (seenAreas.has(area)) {
2043 let areaIsRegistered = gAreas.has(area);
2044 let index = gSavedState.placements[area].indexOf(widget.id);
2046 widgetMightNeedAutoAdding = false;
2047 if (areaIsRegistered) {
2048 widget.currentArea = area;
2049 widget.currentPosition = index;
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
2059 this.beginBatchUpdate();
2061 if (widget.currentArea) {
2062 this.notifyListeners("onWidgetAdded", widget.id, widget.currentArea,
2063 widget.currentPosition);
2064 } else if (widgetMightNeedAutoAdding) {
2067 autoAdd = Services.prefs.getBoolPref(kPrefCustomizationAutoAdd);
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
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);
2080 this.addWidgetToArea(widget.id, widget.defaultArea);
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);
2092 this.notifyListeners("onWidgetAfterCreation", widget.id, widget.currentArea);
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);
2103 ERROR("Error creating builtin widget: " + aData.id);
2107 LOG("Creating built-in widget with id: " + widget.id);
2108 gPalette.set(widget.id, widget);
2111 // Returns true if the area will eventually lazily restore (but hasn't yet).
2112 isAreaLazy: function(aArea) {
2113 if (gPlacements.has(aArea)) {
2116 return gAreas.get(aArea).has("legacy");
2119 //XXXunf Log some warnings here, when the data provided isn't up to scratch.
2120 normalizeWidget: function(aData, aSource) {
2122 implementation: aData,
2123 source: aSource || CustomizableUI.SOURCE_EXTERNAL,
2124 instances: new Map(),
2131 showInPrivateBrowsing: true,
2132 _introducedInVersion: -1,
2135 if (typeof aData.id != "string" || !/^[a-z0-9-_]{1,}$/i.test(aData.id)) {
2136 ERROR("Given an illegal id in normalizeWidget: " + aData.id);
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: "
2150 widget[prop] = aData[prop];
2153 const kOptStringProps = ["label", "tooltiptext", "shortcutId"];
2154 for (let prop of kOptStringProps) {
2155 if (typeof aData[prop] == "string") {
2156 widget[prop] = aData[prop];
2160 const kOptBoolProps = ["removable", "showInPrivateBrowsing", "overflows"];
2161 for (let prop of kOptBoolProps) {
2162 if (typeof aData[prop] == "boolean") {
2163 widget[prop] = aData[prop];
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.");
2178 if ("type" in aData && gSupportedWidgetTypes.has(aData.type)) {
2179 widget.type = aData.type;
2181 widget.type = "button";
2184 widget.disabled = aData.disabled === true;
2186 if (aSource == CustomizableUI.SOURCE_BUILTIN) {
2187 widget._introducedInVersion = aData.introducedInVersion || 0;
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" ?
2198 } else if (widget.type == "view") {
2199 if (typeof aData.viewId != "string") {
2200 ERROR("Expected a string for widget " + widget.id + " viewId, but got "
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);
2212 if (gPalette.has(widget.id)) {
2219 wrapWidgetEventHandler: function(aEventName, aWidget) {
2220 if (typeof aWidget.implementation[aEventName] != "function") {
2221 aWidget[aEventName] = null;
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.
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
2232 return aWidget.implementation[aEventName].apply(aWidget.implementation,
2240 destroyWidget: function(aWidgetId) {
2241 let widget = gPalette.get(aWidgetId);
2243 gGroupWrapperCache.delete(aWidgetId);
2244 for (let [window, ] of gBuildWindows) {
2245 let windowCache = gSingleWrapperCache.get(window);
2247 windowCache.delete(aWidgetId);
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);
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);
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
2270 for (let [window, ] of gBuildWindows) {
2271 let windowCache = gSingleWrapperCache.get(window);
2273 windowCache.delete(aWidgetId);
2275 let widgetNode = window.document.getElementById(aWidgetId) ||
2276 window.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0];
2278 let container = widgetNode.parentNode
2279 this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null,
2281 widgetNode.remove();
2282 this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null,
2285 if (widget.type == "view") {
2286 let viewNode = window.document.getElementById(widget.viewId);
2288 for (let eventName of kSubviewEvents) {
2289 let handler = "on" + eventName;
2290 if (typeof widget[handler] == "function") {
2291 viewNode.removeEventListener(eventName, widget[handler], false);
2298 gPalette.delete(aWidgetId);
2299 gGroupWrapperCache.delete(aWidgetId);
2301 this.notifyListeners("onWidgetDestroyed", aWidgetId);
2304 getCustomizeTargetForArea: function(aArea, aWindow) {
2305 let buildAreaNodes = gBuildAreas.get(aArea);
2306 if (!buildAreaNodes) {
2310 for (let node of buildAreaNodes) {
2311 if (node.ownerDocument.defaultView === aWindow) {
2312 return node.customizationTarget ? node.customizationTarget : node;
2321 this._resetUIState();
2323 // Rebuild each registered area (across windows) to reflect the state that
2325 this._rebuildRegisteredAreas();
2327 for (let [widgetId, widget] of gPalette) {
2328 if (widget.source == CustomizableUI.SOURCE_EXTERNAL) {
2329 gSeenWidgets.add(widgetId);
2332 if (gSeenWidgets.size) {
2339 _resetUIState: function() {
2341 gUIStateBeforeReset.drawInTitlebar = Services.prefs.getBoolPref(kPrefDrawInTitlebar);
2342 gUIStateBeforeReset.deveditionTheme = Services.prefs.getBoolPref(kPrefDeveditionTheme);
2343 gUIStateBeforeReset.uiCustomizationState = Services.prefs.getCharPref(kPrefCustomizationState);
2346 this._resetExtraToolbars();
2348 Services.prefs.clearUserPref(kPrefCustomizationState);
2349 Services.prefs.clearUserPref(kPrefDrawInTitlebar);
2350 Services.prefs.clearUserPref(kPrefDeveditionTheme);
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.
2359 // Restore the state for each area to its defaults
2360 for (let [areaId,] of gAreas) {
2361 this.restoreStateForArea(areaId);
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);
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);
2383 firstWindow = false;
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);
2402 isFirstChangedToolbar = false;
2408 * Undoes a previous reset, restoring the state of the UI to the state prior to the reset.
2410 undoReset: function() {
2411 if (gUIStateBeforeReset.uiCustomizationState == null ||
2412 gUIStateBeforeReset.drawInTitlebar == null ||
2413 gUIStateBeforeReset.deveditionTheme == null) {
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:
2433 for (let areaId of Object.keys(gSavedState.placements)) {
2434 let placements = gSavedState.placements[areaId];
2435 gPlacements.set(areaId, placements);
2437 this._rebuildRegisteredAreas();
2440 gUndoResetting = false;
2443 _clearPreviousUIState: function() {
2444 Object.getOwnPropertyNames(gUIStateBeforeReset).forEach((prop) => {
2445 gUIStateBeforeReset[prop] = null;
2449 removeExtraToolbar: function(aToolbarId) {
2450 this._resetExtraToolbars(aToolbarId);
2454 * @param {String|Node} aWidget - widget ID or a widget node (preferred for performance).
2455 * @return {Boolean} whether the widget is removable
2457 isWidgetRemovable: function(aWidget) {
2460 if (typeof aWidget == "string") {
2463 widgetId = aWidget.id;
2464 widgetNode = aWidget;
2466 let provider = this.getWidgetProvider(widgetId);
2468 if (provider == CustomizableUI.PROVIDER_API) {
2469 return gPalette.get(widgetId).removable;
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.
2480 // Pick any of the build windows to look at.
2481 let [window,] = [...gBuildWindows][0];
2482 [, widgetNode] = this.getWidgetNode(widgetId, window);
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.
2490 return widgetNode.getAttribute("removable") == "true";
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
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) {
2508 // For everything else, just return whether the widget is removable.
2509 return this.isWidgetRemovable(aWidgetId);
2515 ensureWidgetPlacedInWindow: function(aWidgetId, aWindow) {
2516 let placement = this.getPlacementOfWidget(aWidgetId);
2520 let areaNodes = gBuildAreas.get(placement.area);
2524 let container = [...areaNodes].filter((n) => n.ownerDocument.defaultView == aWindow);
2525 if (!container.length) {
2528 let existingNode = container[0].getElementsByAttribute("id", aWidgetId)[0];
2533 this.insertNodeInWindow(aWidgetId, container[0], true);
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) {
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;
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);
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);
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 + ")");
2586 LOG("Checking default state for " + areaId + ":\n" + currentPlacements.join(",") +
2587 "\nvs.\n" + defaultPlacements.join(","));
2589 if (currentPlacements.length != defaultPlacements.length) {
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!");
2602 if (Services.prefs.prefHasUserValue(kPrefDrawInTitlebar)) {
2603 LOG(kPrefDrawInTitlebar + " pref is non-default");
2606 if (Services.prefs.prefHasUserValue(kPrefDeveditionTheme)) {
2607 LOG(kPrefDeveditionTheme + " pref is non-default");
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);
2620 window.setToolbarVisibility(toolbar, aIsVisible, isFirstChangedToolbar);
2621 isFirstChangedToolbar = false;
2626 Object.freeze(CustomizableUIInternal);
2628 this.CustomizableUI = {
2630 * Constant reference to the ID of the menu panel.
2632 get AREA_PANEL() "PanelUI-contents",
2634 * Constant reference to the ID of the navigation toolbar.
2636 get AREA_NAVBAR() "nav-bar",
2638 * Constant reference to the ID of the menubar's toolbar.
2640 get AREA_MENUBAR() "toolbar-menubar",
2642 * Constant reference to the ID of the tabstrip toolbar.
2644 get AREA_TABSTRIP() "TabsToolbar",
2646 * Constant reference to the ID of the bookmarks toolbar.
2648 get AREA_BOOKMARKS() "PersonalToolbar",
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.
2654 get AREA_ADDONBAR() "addon-bar",
2656 * Constant indicating the area is a menu panel.
2658 get TYPE_MENU_PANEL() "menu-panel",
2660 * Constant indicating the area is a toolbar.
2662 get TYPE_TOOLBAR() "toolbar",
2665 * Constant indicating a XUL-type provider.
2667 get PROVIDER_XUL() "xul",
2669 * Constant indicating an API-type provider.
2671 get PROVIDER_API() "api",
2673 * Constant indicating dynamic (special) widgets: spring, spacer, and separator.
2675 get PROVIDER_SPECIAL() "special",
2678 * Constant indicating the widget is built-in
2680 get SOURCE_BUILTIN() "builtin",
2682 * Constant indicating the widget is externally provided
2683 * (e.g. by add-ons or other items not part of the builtin widget set).
2685 get SOURCE_EXTERNAL() "external",
2688 * The class used to distinguish items that span the entire menu panel.
2690 get WIDE_PANEL_CLASS() "panel-wide-item",
2692 * The (constant) number of columns in the menu panel.
2694 get PANEL_COLUMN_COUNT() 3,
2697 * Constant indicating the reason the event was fired was a window closing
2699 get REASON_WINDOW_CLOSED() "window-closed",
2701 * Constant indicating the reason the event was fired was an area being
2702 * unregistered separately from window closing mechanics.
2704 get REASON_AREA_UNREGISTERED() "area-unregistered",
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) { ... }
2713 *[kIteratorSymbol]() {
2714 for (let [window,] of gBuildWindows)
2720 * Add a listener object that will get fired for various events regarding
2723 * @param aListener the listener object to add
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.
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.
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.
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
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.
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.
2791 * - onCustomizeStart(aWindow)
2792 * Fired when opening customize mode in aWindow.
2793 * - onCustomizeEnd(aWindow)
2794 * Fired when exiting customize mode in aWindow.
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
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
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.
2818 addListener: function(aListener) {
2819 CustomizableUIInternal.addListener(aListener);
2822 * Remove a listener added with addListener
2823 * @param aListener the listener object to remove
2825 removeListener: function(aListener) {
2826 CustomizableUIInternal.removeListener(aListener);
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
2835 * - type: the type of area. Either TYPE_TOOLBAR (default) or
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
2851 registerArea: function(aName, aProperties) {
2852 CustomizableUIInternal.registerArea(aName, aProperties);
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
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.
2870 registerToolbarNode: function(aToolbar, aExistingChildren) {
2871 CustomizableUIInternal.registerToolbarNode(aToolbar, aExistingChildren);
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.
2878 registerMenuPanel: function(aPanel) {
2879 CustomizableUIInternal.registerMenuPanel(aPanel);
2882 * Unregister a customizable area. The inverse of registerArea.
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.
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).
2895 * You can override this last behaviour (and destroy the placements
2896 * information in the saved state) by passing true for aDestroyPlacements.
2898 * @param aName the name of the area to unregister
2899 * @param aDestroyPlacements whether to destroy the placements information
2900 * for the area, too.
2902 unregisterArea: function(aName, aDestroyPlacements) {
2903 CustomizableUIInternal.unregisterArea(aName, aDestroyPlacements);
2906 * Add a widget to an area.
2907 * If the area to which you try to add is not known to CustomizableUI,
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
2917 * This will fire an onWidgetAdded notification,
2918 * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification
2919 * for each window CustomizableUI knows about.
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
2927 addWidgetToArea: function(aWidgetId, aArea, aPosition) {
2928 CustomizableUIInternal.addWidgetToArea(aWidgetId, aArea, aPosition);
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
2937 * @param aWidgetId the ID of the widget to remove
2939 removeWidgetFromArea: function(aWidgetId) {
2940 CustomizableUIInternal.removeWidgetFromArea(aWidgetId);
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.
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.
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.
2957 moveWidgetWithinArea: function(aWidgetId, aPosition) {
2958 CustomizableUIInternal.moveWidgetWithinArea(aWidgetId, aPosition);
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
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.
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.
2976 ensureWidgetPlacedInWindow: function(aWidgetId, aWindow) {
2977 return CustomizableUIInternal.ensureWidgetPlacedInWindow(aWidgetId, aWindow);
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.
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.
2991 beginBatchUpdate: function() {
2992 CustomizableUIInternal.beginBatchUpdate();
2995 * End a batch update. See the documentation for beginBatchUpdate above.
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.
3003 * @param aForceDirty force CustomizableUI to flush to the prefs file when
3004 * all batch updates have finished.
3006 endBatchUpdate: function(aForceDirty) {
3007 CustomizableUIInternal.endBatchUpdate(aForceDirty);
3012 * To create a widget, you should pass an object with its desired
3013 * properties. The following properties are supported:
3015 * - id: the ID of the widget (required).
3016 * - type: a string indicating the type of widget. Possible types
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
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
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
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
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)
3064 * @param aProperties the specifications for the widget.
3065 * @return a wrapper around the created widget (see getWidget)
3067 createWidget: function(aProperties) {
3068 return CustomizableUIInternal.wrapWidget(
3069 CustomizableUIInternal.createWidget(aProperties)
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).
3081 * @param aWidgetId the ID of the widget to destroy
3083 destroyWidget: function(aWidgetId) {
3084 CustomizableUIInternal.destroyWidget(aWidgetId);
3087 * Get a wrapper object with information about the widget.
3088 * The object provides the following properties
3089 * (all read-only unless otherwise indicated):
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;
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):
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
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.
3146 getWidget: function(aWidgetId) {
3147 return CustomizableUIInternal.wrapWidget(aWidgetId);
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).
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
3161 * @return an array of widget wrappers (see getWidget)
3163 getUnusedWidgets: function(aWindowPalette) {
3164 return CustomizableUIInternal.getUnusedWidgets(aWindowPalette).map(
3165 CustomizableUIInternal.wrapWidget,
3166 CustomizableUIInternal
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.
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.
3177 * NB: will throw if called too early (before placements have been fetched)
3178 * or if the area is not currently known to CustomizableUI.
3180 getWidgetIdsInArea: function(aArea) {
3181 if (!gAreas.has(aArea)) {
3182 throw new Error("Unknown customization area: " + aArea);
3184 if (!gPlacements.has(aArea)) {
3185 throw new Error("Area not yet restored");
3188 // We need to clone this, as we don't want to let consumers muck with placements
3189 return [...gPlacements.get(aArea)];
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.
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.
3203 * NB: will throw if called too early (before placements have been fetched)
3204 * or if the area is not currently known to CustomizableUI.
3206 getWidgetsInArea: function(aArea) {
3207 return this.getWidgetIdsInArea(aArea).map(
3208 CustomizableUIInternal.wrapWidget,
3209 CustomizableUIInternal
3213 * Obtain an array of all the area IDs known to CustomizableUI.
3214 * This array is created for you, so is modifiable without CustomizableUI
3218 return [area for ([area, props] of gAreas)];
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.
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.
3230 getAreaType: function(aArea) {
3231 let area = gAreas.get(aArea);
3232 return area ? area.get("type") : null;
3235 * Check if a toolbar is collapsed by default.
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
3241 isToolbarDefaultCollapsed: function(aArea) {
3242 let area = gAreas.get(aArea);
3243 return area ? area.get("defaultCollapsed") : null;
3246 * Obtain the DOM node that is the customize target for an area in a
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.
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.
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
3269 getCustomizeTargetForArea: function(aArea, aWindow) {
3270 return CustomizableUIInternal.getCustomizeTargetForArea(aArea, aWindow);
3273 * Reset the customization state back to its default.
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.
3280 CustomizableUIInternal.reset();
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.
3287 undoReset: function() {
3288 CustomizableUIInternal.undoReset();
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
3296 * @param aToolbarId the ID of the toolbar to remove
3298 removeExtraToolbar: function(aToolbarId) {
3299 CustomizableUIInternal.removeExtraToolbar(aToolbarId);
3303 * Can the last Restore Defaults operation be undone.
3305 * @return A boolean stating whether an undo of the
3306 * Restore Defaults can be performed.
3308 get canUndoReset() {
3309 return gUIStateBeforeReset.uiCustomizationState != null ||
3310 gUIStateBeforeReset.drawInTitlebar != null ||
3311 gUIStateBeforeReset.deveditionTheme != null;
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.
3320 * @param aWidgetId the ID of the widget whose placement you want to know
3323 * area: "somearea", // The ID of the area where the widget is placed
3324 * position: 42 // the index in the placements array corresponding to
3330 * null // if the widget is not placed anywhere (ie in the palette)
3332 getPlacementOfWidget: function(aWidgetId) {
3333 return CustomizableUIInternal.getPlacementOfWidget(aWidgetId, true);
3336 * Check if a widget can be removed from the area it's in.
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 (!).
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.
3349 * @param aWidgetId a widget ID or DOM node to check
3350 * @return true if the widget can be removed from its area,
3353 isWidgetRemovable: function(aWidgetId) {
3354 return CustomizableUIInternal.isWidgetRemovable(aWidgetId);
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.
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.
3366 canWidgetMoveToArea: function(aWidgetId, aArea) {
3367 return CustomizableUIInternal.canWidgetMoveToArea(aWidgetId, aArea);
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.
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.
3378 get inDefaultState() {
3379 return CustomizableUIInternal.inDefaultState;
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
3387 setToolbarVisibility: function(aToolbarId, aIsVisible) {
3388 CustomizableUIInternal.setToolbarVisibility(aToolbarId, aIsVisible);
3392 * Get a localized property off a (widget?) object.
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.
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
3405 * @param aDef (optional) the default to return if we don't find the
3406 * string in the stringbundle;
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
3414 getLocalizedProperty: function(aWidget, aProp, aFormatArgs, aDef) {
3415 return CustomizableUIInternal.getLocalizedProperty(aWidget, aProp,
3419 * Utility function to detect, find and set a keyboard shortcut for a menuitem
3420 * or (toolbar)button.
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;
3427 addShortcut: function(aShortcutNode, aTargetNode) {
3428 return CustomizableUIInternal.addShortcut(aShortcutNode, aTargetNode);
3431 * Given a node, walk up to the first panel in its ancestor chain, and
3434 * @param aNode a node whose panel should be closed;
3436 hidePanelForNode: function(aNode) {
3437 CustomizableUIInternal.hidePanelForNode(aNode);
3440 * Check if a widget is a "special" widget: a spring, spacer or separator.
3442 * @param aWidgetId the widget ID to check.
3443 * @return true if the widget is 'special', false otherwise.
3445 isSpecialWidget: function(aWidgetId) {
3446 return CustomizableUIInternal.isSpecialWidget(aWidgetId);
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
3453 * @param aPanel the panel to which listeners should be attached.
3455 addPanelCloseListeners: function(aPanel) {
3456 CustomizableUIInternal.addPanelCloseListeners(aPanel);
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.
3463 * @param aPanel the panel from which listeners should be removed.
3465 removePanelCloseListeners: function(aPanel) {
3466 CustomizableUIInternal.removePanelCloseListeners(aPanel);
3469 * Notify listeners a widget is about to be dragged to an area. For use from
3470 * Customize Mode only, do not use otherwise.
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.
3475 onWidgetDrag: function(aWidgetId, aArea) {
3476 CustomizableUIInternal.notifyListeners("onWidgetDrag", aWidgetId, aArea);
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
3483 notifyStartCustomizing: function(aWindow) {
3484 CustomizableUIInternal.notifyListeners("onCustomizeStart", aWindow);
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
3491 notifyEndCustomizing: function(aWindow) {
3492 CustomizableUIInternal.notifyListeners("onCustomizeEnd", aWindow);
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.
3503 dispatchToolboxEvent: function(aEvent, aDetails={}, aWindow=null) {
3504 CustomizableUIInternal.dispatchToolboxEvent(aEvent, aDetails, aWindow);
3508 * Check whether an area is overflowable.
3510 * @param aAreaId the ID of an area to check for overflowable-ness
3511 * @return true if the area is overflowable, false otherwise.
3513 isAreaOverflowable: function(aAreaId) {
3514 let area = gAreas.get(aAreaId);
3515 return area ? area.get("type") == this.TYPE_TOOLBAR && area.get("overflowable")
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.
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.
3528 getPlaceForItem: function(aElement) {
3530 let node = aElement;
3531 while (node && !place) {
3532 if (node.localName == "toolbar")
3534 else if (node.id == CustomizableUI.AREA_PANEL)
3536 else if (node.id == "customization-palette")
3539 node = node.parentNode;
3545 * Check if a toolbar is builtin or not.
3546 * @param aToolbarId the ID of the toolbar you want to check
3548 isBuiltinToolbar: function(aToolbarId) {
3549 return CustomizableUIInternal._builtinToolbars.has(aToolbarId);
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.
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
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]);
3576 this.__defineGetter__("provider", function() CustomizableUI.PROVIDER_API);
3578 this.__defineSetter__("disabled", function(aValue) {
3580 aWidget.disabled = aValue;
3581 for (let [,instance] of aWidget.instances) {
3582 instance.disabled = aValue;
3586 this.forWindow = function WidgetGroupWrapper_forWindow(aWindow) {
3588 if (!gSingleWrapperCache.has(aWindow)) {
3589 wrapperMap = new Map();
3590 gSingleWrapperCache.set(aWindow, wrapperMap);
3592 wrapperMap = gSingleWrapperCache.get(aWindow);
3594 if (wrapperMap.has(aWidget.id)) {
3595 return wrapperMap.get(aWidget.id);
3598 let instance = aWidget.instances.get(aWindow.document);
3600 (aWidget.showInPrivateBrowsing || !PrivateBrowsingUtils.isWindowPrivate(aWindow))) {
3601 instance = CustomizableUIInternal.buildWidget(aWindow.document,
3605 let wrapper = new WidgetSingleWrapper(aWidget, instance);
3606 wrapperMap.set(aWidget.id, wrapper);
3610 this.__defineGetter__("instances", function() {
3611 // Can't use gBuildWindows here because some areas load lazily:
3612 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id);
3616 let area = placement.area;
3617 let buildAreas = gBuildAreas.get(area);
3621 return [this.forWindow(node.ownerDocument.defaultView) for (node of buildAreas)];
3624 this.__defineGetter__("areaType", function() {
3625 let areaProps = gAreas.get(aWidget.currentArea);
3626 return areaProps && areaProps.get("type");
3629 Object.freeze(this);
3633 * A WidgetSingleWrapper is a wrapper around a single instance of a widget in
3634 * a particular window.
3636 function WidgetSingleWrapper(aWidget, aNode) {
3637 this.isGroup = false;
3640 this.provider = CustomizableUI.PROVIDER_API;
3642 const kGlobalProps = ["id", "type"];
3643 for (let prop of kGlobalProps) {
3644 this[prop] = aWidget[prop];
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));
3656 this.__defineGetter__("disabled", function() aNode.disabled);
3657 this.__defineSetter__("disabled", function(aValue) {
3658 aNode.disabled = !!aValue;
3661 this.__defineGetter__("anchor", function() {
3663 // First check for an anchor for the area:
3664 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id);
3666 anchorId = gAreas.get(placement.area).get("anchor");
3669 anchorId = aNode.getAttribute("cui-anchorid");
3672 return anchorId ? aNode.ownerDocument.getElementById(anchorId)
3676 this.__defineGetter__("overflowed", function() {
3677 return aNode.getAttribute("overflowedItem") == "true";
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
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) {
3699 if (!gSingleWrapperCache.has(aWindow)) {
3700 wrapperMap = new Map();
3701 gSingleWrapperCache.set(aWindow, wrapperMap);
3703 wrapperMap = gSingleWrapperCache.get(aWindow);
3705 if (wrapperMap.has(aWidgetId)) {
3706 return wrapperMap.get(aWidgetId);
3709 let instance = aWindow.document.getElementById(aWidgetId);
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];
3716 let wrapper = new XULWidgetSingleWrapper(aWidgetId, instance, aWindow.document);
3717 wrapperMap.set(aWidgetId, wrapper);
3721 this.__defineGetter__("areaType", function() {
3722 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId);
3727 let areaProps = gAreas.get(placement.area);
3728 return areaProps && areaProps.get("type");
3731 this.__defineGetter__("instances", function() {
3732 return [this.forWindow(win) for ([win,] of gBuildWindows)];
3735 Object.freeze(this);
3739 * A XULWidgetSingleWrapper is a wrapper around a single instance of a XUL
3740 * widget in a particular window.
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:
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:
3760 // Return the last known node if it's still in the DOM...
3761 if (aNode.ownerDocument.contains(aNode)) {
3764 // ... or the toolbox
3765 let toolbox = aNode.ownerDocument.defaultView.gNavToolbox;
3766 if (toolbox && toolbox.palette && aNode.parentNode == toolbox.palette) {
3769 // If it isn't, clear the cached value and fall through to the "slow" case:
3773 let doc = weakDoc.get();
3775 // Store locally so we can cache the result:
3776 aNode = CustomizableUIInternal.findWidgetInWindow(aWidgetId, doc.defaultView);
3779 // The weakref to the document is dead, we're done here forever more:
3784 this.__defineGetter__("anchor", function() {
3786 // First check for an anchor for the area:
3787 let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId);
3789 anchorId = gAreas.get(placement.area).get("anchor");
3792 let node = this.node;
3793 if (!anchorId && node) {
3794 anchorId = node.getAttribute("cui-anchorid");
3797 return (anchorId && node) ? node.ownerDocument.getElementById(anchorId) : node;
3800 this.__defineGetter__("overflowed", function() {
3801 let node = this.node;
3805 return node.getAttribute("overflowedItem") == "true";
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) {
3830 Services.obs.addObserver(this, "browser-delayed-startup-finished", false);
3834 OverflowableToolbar.prototype = {
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");
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;
3872 this.initialized = true;
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");
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);
3899 handleEvent: function(aEvent) {
3900 switch(aEvent.type) {
3901 case "aftercustomization":
3905 if (aEvent.target == this._chevron) {
3906 this._onClickChevron(aEvent);
3908 this._panel.hidePopup();
3911 case "customizationstarting":
3915 this._showWithTimeout();
3918 this._panel.hidePopup();
3921 this._onPanelHiding(aEvent);
3924 this._onResize(aEvent);
3929 let deferred = Promise.defer();
3930 if (this._panel.state == "open") {
3932 return deferred.promise;
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);
3950 return deferred.promise;
3953 _onClickChevron: function(aEvent) {
3954 if (this._chevron.open) {
3955 this._panel.hidePopup();
3956 this._chevron.open = false;
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);
3971 onOverflow: function(aEvent) {
3972 if (!this._enabled ||
3973 (aEvent && aEvent.target != this._toolbar.customizationTarget))
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);
3991 this._toolbar.setAttribute("overflowing", "true");
3996 let win = this._target.ownerDocument.defaultView;
3997 win.UpdateUrlbarSearchSplitterState();
4000 _onResize: function(aEvent) {
4001 if (!this._lazyResizeHandler) {
4002 this._lazyResizeHandler = new DeferredTask(this._onLazyResize.bind(this),
4003 LAZY_RESIZE_INTERVAL_MS);
4005 this._lazyResizeHandler.arm();
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 &&
4016 this._target.clientWidth <= minSize) {
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;
4028 let inserted = false;
4029 for (; beforeNodeIndex < placements.length; beforeNodeIndex++) {
4030 let beforeNode = this._target.getElementsByAttribute("id", placements[beforeNodeIndex])[0];
4032 this._target.insertBefore(child, beforeNode);
4038 this._target.appendChild(child);
4040 child.removeAttribute("cui-anchorid");
4041 child.removeAttribute("overflowedItem");
4042 CustomizableUIInternal.notifyListeners("onWidgetUnderflow", child, this._target);
4045 let win = this._target.ownerDocument.defaultView;
4046 win.UpdateUrlbarSearchSplitterState();
4048 if (!this._collapsed.size) {
4049 this._toolbar.removeAttribute("overflowing");
4050 CustomizableUI.removeListener(this);
4054 _onLazyResize: function() {
4058 if (this._target.scrollLeftMax > 0) {
4061 this._moveItemsBackToTheirOrigin();
4065 _disable: function() {
4066 this._enabled = false;
4067 this._moveItemsBackToTheirOrigin(true);
4068 if (this._lazyResizeHandler) {
4069 this._lazyResizeHandler.disarm();
4073 _enable: function() {
4074 this._enabled = true;
4078 onWidgetBeforeDOMChange: function(aNode, aNextNode, aContainer) {
4079 if (aContainer != this._target && aContainer != this._list) {
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) {
4087 if (aNode.previousSibling) {
4088 updatedMinSize = this._collapsed.get(aNode.previousSibling.id);
4090 // Force (these) items to try to flow back into the bar:
4093 let nextItem = aNode.nextSibling;
4095 this._collapsed.set(nextItem.id, updatedMinSize);
4096 nextItem = nextItem.nextSibling;
4101 onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer) {
4102 if (aContainer != this._target && aContainer != this._list) {
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);
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);
4129 // If it's in the toolbar now, then we don't care. An overflow event may
4130 // fire afterwards; that's ok!
4132 // If it used to be overflowed...
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);
4146 // but if it still is, it must have changed places. Bookkeep:
4148 if (aNode.previousSibling) {
4149 let prevId = aNode.previousSibling.id;
4150 let minSize = this._collapsed.get(prevId);
4151 this._collapsed.set(aNode.id, minSize);
4153 // If it's now the first item in the overflow list,
4154 // maybe we can return it:
4155 this._moveItemsBackToTheirOrigin();
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;
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);
4175 return [this._list, nextNode];
4178 if (!nodeBeforeNewNodeIsOverflown || !newNodeCanOverflow) {
4179 let nextNode = this._target.getElementsByAttribute("id", nextNodeId).item(0);
4181 return [this._target, nextNode];
4184 } else if (loopIndex < nodeIndex && this._collapsed.has(nextNodeId)) {
4185 nodeBeforeNewNodeIsOverflown = true;
4189 let containerForAppending = (this._collapsed.size && newNodeCanOverflow) ?
4190 this._list : this._target;
4191 return [containerForAppending, null];
4194 getContainerFor: function(aNode) {
4195 if (aNode.getAttribute("overflowedItem") == "true") {
4198 return this._target;
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);
4208 this._hideTimeoutId = window.setTimeout(() => {
4209 if (!this._panel.firstChild.matches(":hover")) {
4210 this._panel.hidePopup();
4212 }, OVERFLOW_PANEL_HIDE_DELAY_MS);
4217 CustomizableUIInternal.initialize();