Mirror of roytam1's UXP fork just in case Moonchild and Tobin decide to go after him
You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
 
 
 
 
 
 

4387 lines
156 KiB

/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["CustomizableUI"];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
Cu.import("resource://gre/modules/AppConstants.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PanelWideWidgetTracker",
"resource:///modules/PanelWideWidgetTracker.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableWidgets",
"resource:///modules/CustomizableWidgets.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask",
"resource://gre/modules/DeferredTask.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils",
"resource://gre/modules/PrivateBrowsingUtils.jsm");
XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() {
const kUrl = "chrome://browser/locale/customizableui/customizableWidgets.properties";
return Services.strings.createBundle(kUrl);
});
XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
"resource://gre/modules/ShortcutUtils.jsm");
XPCOMUtils.defineLazyServiceGetter(this, "gELS",
"@mozilla.org/eventlistenerservice;1", "nsIEventListenerService");
XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager",
"resource://gre/modules/LightweightThemeManager.jsm");
const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
const kSpecialWidgetPfx = "customizableui-special-";
const kPrefCustomizationState = "browser.uiCustomization.state";
const kPrefCustomizationAutoAdd = "browser.uiCustomization.autoAdd";
const kPrefCustomizationDebug = "browser.uiCustomization.debug";
const kPrefDrawInTitlebar = "browser.tabs.drawInTitlebar";
const kExpectedWindowURL = "chrome://browser/content/browser.xul";
/**
* The keys are the handlers that are fired when the event type (the value)
* is fired on the subview. A widget that provides a subview has the option
* of providing onViewShowing and onViewHiding event handlers.
*/
const kSubviewEvents = [
"ViewShowing",
"ViewHiding"
];
/**
* The current version. We can use this to auto-add new default widgets as necessary.
* (would be const but isn't because of testing purposes)
*/
var kVersion = 6;
/**
* Buttons removed from built-ins by version they were removed. kVersion must be
* bumped any time a new id is added to this. Use the button id as key, and
* version the button is removed in as the value. e.g. "pocket-button": 5
*/
var ObsoleteBuiltinButtons = {
"pocket-button": 6
};
/**
* gPalette is a map of every widget that CustomizableUI.jsm knows about, keyed
* on their IDs.
*/
var gPalette = new Map();
/**
* gAreas maps area IDs to Sets of properties about those areas. An area is a
* place where a widget can be put.
*/
var gAreas = new Map();
/**
* gPlacements maps area IDs to Arrays of widget IDs, indicating that the widgets
* are placed within that area (either directly in the area node, or in the
* customizationTarget of the node).
*/
var gPlacements = new Map();
/**
* gFuturePlacements represent placements that will happen for areas that have
* not yet loaded (due to lazy-loading). This can occur when add-ons register
* widgets.
*/
var gFuturePlacements = new Map();
// XXXunf Temporary. Need a nice way to abstract functions to build widgets
// of these types.
var gSupportedWidgetTypes = new Set(["button", "view", "custom"]);
/**
* gPanelsForWindow is a list of known panels in a window which we may need to close
* should command events fire which target them.
*/
var gPanelsForWindow = new WeakMap();
/**
* gSeenWidgets remembers which widgets the user has seen for the first time
* before. This way, if a new widget is created, and the user has not seen it
* before, it can be put in its default location. Otherwise, it remains in the
* palette.
*/
var gSeenWidgets = new Set();
/**
* gDirtyAreaCache is a set of area IDs for areas where items have been added,
* moved or removed at least once. This set is persisted, and is used to
* optimize building of toolbars in the default case where no toolbars should
* be "dirty".
*/
var gDirtyAreaCache = new Set();
/**
* gPendingBuildAreas is a map from area IDs to map from build nodes to their
* existing children at the time of node registration, that are waiting
* for the area to be registered
*/
var gPendingBuildAreas = new Map();
var gSavedState = null;
var gRestoring = false;
var gDirty = false;
var gInBatchStack = 0;
var gResetting = false;
var gUndoResetting = false;
/**
* gBuildAreas maps area IDs to actual area nodes within browser windows.
*/
var gBuildAreas = new Map();
/**
* gBuildWindows is a map of windows that have registered build areas, mapped
* to a Set of known toolboxes in that window.
*/
var gBuildWindows = new Map();
var gNewElementCount = 0;
var gGroupWrapperCache = new Map();
var gSingleWrapperCache = new WeakMap();
var gListeners = new Set();
var gUIStateBeforeReset = {
uiCustomizationState: null,
drawInTitlebar: null,
currentTheme: null,
};
XPCOMUtils.defineLazyGetter(this, "log", () => {
let scope = {};
Cu.import("resource://gre/modules/Console.jsm", scope);
let debug = Services.prefs.getBoolPref(kPrefCustomizationDebug, false);
let consoleOptions = {
maxLogLevel: debug ? "all" : "log",
prefix: "CustomizableUI",
};
return new scope.ConsoleAPI(consoleOptions);
});
var CustomizableUIInternal = {
initialize: function() {
log.debug("Initializing");
this.addListener(this);
this._defineBuiltInWidgets();
this.loadSavedState();
this._introduceNewBuiltinWidgets();
this._markObsoleteBuiltinButtonsSeen();
/**
* Please be advised that adding items to the panel by default could
* cause CART talos test regressions. This might happen when the
* number of items in the panel causes the area to become "scrollable"
* during the last phases of the transition. See bug 1230671 for an
* example of this. Be sure that what you're adding really needs to go
* into the panel by default, and if it does, consider swapping
* something out for it.
*/
let panelPlacements = [
"edit-controls",
"zoom-controls",
"new-window-button",
"privatebrowsing-button",
"save-page-button",
"print-button",
"history-panelmenu",
"fullscreen-button",
"find-button",
"preferences-button",
"add-ons-button",
];
panelPlacements.splice(-1, 0, "developer-button");
let showCharacterEncoding = Services.prefs.getComplexValue(
"browser.menu.showCharacterEncoding",
Ci.nsIPrefLocalizedString
).data;
if (showCharacterEncoding == "true") {
panelPlacements.push("characterencoding-button");
}
this.registerArea(CustomizableUI.AREA_PANEL, {
anchor: "PanelUI-menu-button",
type: CustomizableUI.TYPE_MENU_PANEL,
defaultPlacements: panelPlacements
}, true);
PanelWideWidgetTracker.init();
let navbarPlacements = [
"urlbar-container",
"search-container",
"bookmarks-menu-button",
"downloads-button",
"home-button",
];
// Place this last, when createWidget is called for pocket, it will
// append to the toolbar.
if (Services.prefs.getPrefType("extensions.pocket.enabled") != Services.prefs.PREF_INVALID &&
Services.prefs.getBoolPref("extensions.pocket.enabled")) {
navbarPlacements.push("pocket-button");
}
this.registerArea(CustomizableUI.AREA_NAVBAR, {
legacy: true,
type: CustomizableUI.TYPE_TOOLBAR,
overflowable: true,
defaultPlacements: navbarPlacements,
defaultCollapsed: false,
}, true);
if (AppConstants.platform != "macosx") {
this.registerArea(CustomizableUI.AREA_MENUBAR, {
legacy: true,
type: CustomizableUI.TYPE_TOOLBAR,
defaultPlacements: [
"menubar-items",
],
get defaultCollapsed() {
if (AppConstants.MENUBAR_CAN_AUTOHIDE) {
if (AppConstants.platform == "linux") {
return true;
}
// This is duplicated logic from /browser/base/jar.mn
// for win6BrowserOverlay.xul.
return AppConstants.isPlatformAndVersionAtLeast("win", 6);
}
return false;
}
}, true);
}
this.registerArea(CustomizableUI.AREA_TABSTRIP, {
legacy: true,
type: CustomizableUI.TYPE_TOOLBAR,
defaultPlacements: [
"tabbrowser-tabs",
"new-tab-button",
"alltabs-button",
],
defaultCollapsed: null,
}, true);
this.registerArea(CustomizableUI.AREA_BOOKMARKS, {
legacy: true,
type: CustomizableUI.TYPE_TOOLBAR,
defaultPlacements: [
"personal-bookmarks",
],
defaultCollapsed: true,
}, true);
this.registerArea(CustomizableUI.AREA_ADDONBAR, {
type: CustomizableUI.TYPE_TOOLBAR,
legacy: true,
defaultPlacements: ["addonbar-closebutton", "status-bar"],
defaultCollapsed: false,
}, true);
},
get _builtinToolbars() {
let toolbars = new Set([
CustomizableUI.AREA_NAVBAR,
CustomizableUI.AREA_BOOKMARKS,
CustomizableUI.AREA_TABSTRIP,
CustomizableUI.AREA_ADDONBAR,
]);
if (AppConstants.platform != "macosx") {
toolbars.add(CustomizableUI.AREA_MENUBAR);
}
return toolbars;
},
_defineBuiltInWidgets: function() {
for (let widgetDefinition of CustomizableWidgets) {
this.createBuiltinWidget(widgetDefinition);
}
},
_introduceNewBuiltinWidgets: function() {
// We should still enter even if gSavedState.currentVersion >= kVersion
// because the per-widget pref facility is independent of versioning.
if (!gSavedState) {
// Flip all the prefs so we don't try to re-introduce later:
for (let [, widget] of gPalette) {
if (widget.defaultArea && widget._introducedInVersion === "pref") {
let prefId = "browser.toolbarbuttons.introduced." + widget.id;
Services.prefs.setBoolPref(prefId, true);
}
}
return;
}
let currentVersion = gSavedState.currentVersion;
for (let [id, widget] of gPalette) {
if (widget.defaultArea) {
let shouldAdd = false;
let shouldSetPref = false;
let prefId = "browser.toolbarbuttons.introduced." + widget.id;
if (widget._introducedInVersion === "pref") {
try {
shouldAdd = !Services.prefs.getBoolPref(prefId);
} catch (ex) {
// Pref doesn't exist:
shouldAdd = true;
}
shouldSetPref = shouldAdd;
} else if (widget._introducedInVersion > currentVersion) {
shouldAdd = true;
}
if (shouldAdd) {
let futurePlacements = gFuturePlacements.get(widget.defaultArea);
if (futurePlacements) {
futurePlacements.add(id);
} else {
gFuturePlacements.set(widget.defaultArea, new Set([id]));
}
if (shouldSetPref) {
Services.prefs.setBoolPref(prefId, true);
}
}
}
}
if (currentVersion < 2) {
// Nuke the old 'loop-call-button' out of orbit.
CustomizableUI.removeWidgetFromArea("loop-call-button");
}
if (currentVersion < 4) {
CustomizableUI.removeWidgetFromArea("loop-button-throttled");
}
},
/**
* _markObsoleteBuiltinButtonsSeen
* when upgrading, ensure obsoleted buttons are in seen state.
*/
_markObsoleteBuiltinButtonsSeen: function() {
if (!gSavedState)
return;
let currentVersion = gSavedState.currentVersion;
if (currentVersion >= kVersion)
return;
// we're upgrading, update state if necessary
for (let id in ObsoleteBuiltinButtons) {
let version = ObsoleteBuiltinButtons[id]
if (version == kVersion) {
gSeenWidgets.add(id);
gDirty = true;
}
}
},
_placeNewDefaultWidgetsInArea: function(aArea) {
let futurePlacedWidgets = gFuturePlacements.get(aArea);
let savedPlacements = gSavedState && gSavedState.placements && gSavedState.placements[aArea];
let defaultPlacements = gAreas.get(aArea).get("defaultPlacements");
if (!savedPlacements || !savedPlacements.length || !futurePlacedWidgets || !defaultPlacements ||
!defaultPlacements.length) {
return;
}
let defaultWidgetIndex = -1;
for (let widgetId of futurePlacedWidgets) {
let widget = gPalette.get(widgetId);
if (!widget || widget.source !== CustomizableUI.SOURCE_BUILTIN ||
!widget.defaultArea || !widget._introducedInVersion ||
savedPlacements.indexOf(widget.id) !== -1) {
continue;
}
defaultWidgetIndex = defaultPlacements.indexOf(widget.id);
if (defaultWidgetIndex === -1) {
continue;
}
// Now we know that this widget should be here by default, was newly introduced,
// and we have a saved state to insert into, and a default state to work off of.
// Try introducing after widgets that come before it in the default placements:
for (let i = defaultWidgetIndex; i >= 0; i--) {
// Special case: if the defaults list this widget as coming first, insert at the beginning:
if (i === 0 && i === defaultWidgetIndex) {
savedPlacements.splice(0, 0, widget.id);
// Before you ask, yes, deleting things inside a let x of y loop where y is a Set is
// safe, and we won't skip any items.
futurePlacedWidgets.delete(widget.id);
gDirty = true;
break;
}
// Otherwise, if we're somewhere other than the beginning, check if the previous
// widget is in the saved placements.
if (i) {
let previousWidget = defaultPlacements[i - 1];
let previousWidgetIndex = savedPlacements.indexOf(previousWidget);
if (previousWidgetIndex != -1) {
savedPlacements.splice(previousWidgetIndex + 1, 0, widget.id);
futurePlacedWidgets.delete(widget.id);
gDirty = true;
break;
}
}
}
// The loop above either inserts the item or doesn't - either way, we can get away
// with doing nothing else now; if the item remains in gFuturePlacements, we'll
// add it at the end in restoreStateForArea.
}
this.saveState();
},
wrapWidget: function(aWidgetId) {
if (gGroupWrapperCache.has(aWidgetId)) {
return gGroupWrapperCache.get(aWidgetId);
}
let provider = this.getWidgetProvider(aWidgetId);
if (!provider) {
return null;
}
if (provider == CustomizableUI.PROVIDER_API) {
let widget = gPalette.get(aWidgetId);
if (!widget.wrapper) {
widget.wrapper = new WidgetGroupWrapper(widget);
gGroupWrapperCache.set(aWidgetId, widget.wrapper);
}
return widget.wrapper;
}
// PROVIDER_SPECIAL gets treated the same as PROVIDER_XUL.
let wrapper = new XULWidgetGroupWrapper(aWidgetId);
gGroupWrapperCache.set(aWidgetId, wrapper);
return wrapper;
},
registerArea: function(aName, aProperties, aInternalCaller) {
if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
throw new Error("Invalid area name");
}
let areaIsKnown = gAreas.has(aName);
let props = areaIsKnown ? gAreas.get(aName) : new Map();
const kImmutableProperties = new Set(["type", "legacy", "overflowable"]);
for (let key in aProperties) {
if (areaIsKnown && kImmutableProperties.has(key) &&
props.get(key) != aProperties[key]) {
throw new Error("An area cannot change the property for '" + key + "'");
}
// XXXgijs for special items, we need to make sure they have an appropriate ID
// so we aren't perpetually in a non-default state:
if (key == "defaultPlacements" && Array.isArray(aProperties[key])) {
props.set(key, aProperties[key].map(x => this.isSpecialWidget(x) ? this.ensureSpecialWidgetId(x) : x ));
} else {
props.set(key, aProperties[key]);
}
}
// Default to a toolbar:
if (!props.has("type")) {
props.set("type", CustomizableUI.TYPE_TOOLBAR);
}
if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) {
// Check aProperties instead of props because this check is only interested
// in the passed arguments, not the state of a potentially pre-existing area.
if (!aInternalCaller && aProperties["defaultCollapsed"]) {
throw new Error("defaultCollapsed is only allowed for default toolbars.")
}
if (!props.has("defaultCollapsed")) {
props.set("defaultCollapsed", true);
}
} else if (props.has("defaultCollapsed")) {
throw new Error("defaultCollapsed only applies for TYPE_TOOLBAR areas.");
}
// Sanity check type:
let allTypes = [CustomizableUI.TYPE_TOOLBAR, CustomizableUI.TYPE_MENU_PANEL];
if (allTypes.indexOf(props.get("type")) == -1) {
throw new Error("Invalid area type " + props.get("type"));
}
// And to no placements:
if (!props.has("defaultPlacements")) {
props.set("defaultPlacements", []);
}
// Sanity check default placements array:
if (!Array.isArray(props.get("defaultPlacements"))) {
throw new Error("Should provide an array of default placements");
}
if (!areaIsKnown) {
gAreas.set(aName, props);
// Reconcile new default widgets. Have to do this before we start restoring things.
this._placeNewDefaultWidgetsInArea(aName);
if (props.get("legacy") && !gPlacements.has(aName)) {
// Guarantee this area exists in gFuturePlacements, to avoid checking it in
// various places elsewhere.
if (!gFuturePlacements.has(aName)) {
gFuturePlacements.set(aName, new Set());
}
} else {
this.restoreStateForArea(aName);
}
// If we have pending build area nodes, register all of them
if (gPendingBuildAreas.has(aName)) {
let pendingNodes = gPendingBuildAreas.get(aName);
for (let [pendingNode, existingChildren] of pendingNodes) {
this.registerToolbarNode(pendingNode, existingChildren);
}
gPendingBuildAreas.delete(aName);
}
}
},
unregisterArea: function(aName, aDestroyPlacements) {
if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) {
throw new Error("Invalid area name");
}
if (!gAreas.has(aName) && !gPlacements.has(aName)) {
throw new Error("Area not registered");
}
// Move all the widgets out
this.beginBatchUpdate();
try {
let placements = gPlacements.get(aName);
if (placements) {
// Need to clone this array so removeWidgetFromArea doesn't modify it
placements = [...placements];
placements.forEach(this.removeWidgetFromArea, this);
}
// Delete all remaining traces.
gAreas.delete(aName);
// Only destroy placements when necessary:
if (aDestroyPlacements) {
gPlacements.delete(aName);
} else {
// Otherwise we need to re-set them, as removeFromArea will have emptied
// them out:
gPlacements.set(aName, placements);
}
gFuturePlacements.delete(aName);
let existingAreaNodes = gBuildAreas.get(aName);
if (existingAreaNodes) {
for (let areaNode of existingAreaNodes) {
this.notifyListeners("onAreaNodeUnregistered", aName, areaNode.customizationTarget,
CustomizableUI.REASON_AREA_UNREGISTERED);
}
}
gBuildAreas.delete(aName);
} finally {
this.endBatchUpdate(true);
}
},
registerToolbarNode: function(aToolbar, aExistingChildren) {
let area = aToolbar.id;
if (gBuildAreas.has(area) && gBuildAreas.get(area).has(aToolbar)) {
return;
}
let areaProperties = gAreas.get(area);
// If this area is not registered, try to do it automatically:
if (!areaProperties) {
// If there's no defaultset attribute and this isn't a legacy extra toolbar,
// we assume that we should wait for registerArea to be called:
if (!aToolbar.hasAttribute("defaultset") &&
!aToolbar.hasAttribute("customindex")) {
if (!gPendingBuildAreas.has(area)) {
gPendingBuildAreas.set(area, new Map());
}
let pendingNodes = gPendingBuildAreas.get(area);
pendingNodes.set(aToolbar, aExistingChildren);
return;
}
let props = {type: CustomizableUI.TYPE_TOOLBAR, legacy: true};
let defaultsetAttribute = aToolbar.getAttribute("defaultset") || "";
props.defaultPlacements = defaultsetAttribute.split(',').filter(s => s);
this.registerArea(area, props);
areaProperties = gAreas.get(area);
}
this.beginBatchUpdate();
try {
let placements = gPlacements.get(area);
if (!placements && areaProperties.has("legacy")) {
let legacyState = aToolbar.getAttribute("currentset");
if (legacyState) {
legacyState = legacyState.split(",").filter(s => s);
}
// Manually restore the state here, so the legacy state can be converted.
this.restoreStateForArea(area, legacyState);
placements = gPlacements.get(area);
}
// Check that the current children and the current placements match. If
// not, mark it as dirty:
if (aExistingChildren.length != placements.length ||
aExistingChildren.every((id, i) => id == placements[i])) {
gDirtyAreaCache.add(area);
}
if (areaProperties.has("overflowable")) {
aToolbar.overflowable = new OverflowableToolbar(aToolbar);
}
this.registerBuildArea(area, aToolbar);
// We only build the toolbar if it's been marked as "dirty". Dirty means
// one of the following things:
// 1) Items have been added, moved or removed from this toolbar before.
// 2) The number of children of the toolbar does not match the length of
// the placements array for that area.
//
// This notion of being "dirty" is stored in a cache which is persisted
// in the saved state.
if (gDirtyAreaCache.has(area)) {
this.buildArea(area, placements, aToolbar);
}
this.notifyListeners("onAreaNodeRegistered", area, aToolbar.customizationTarget);
aToolbar.setAttribute("currentset", placements.join(","));
} finally {
this.endBatchUpdate();
}
},
buildArea: function(aArea, aPlacements, aAreaNode) {
let document = aAreaNode.ownerDocument;
let window = document.defaultView;
let inPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(window);
let container = aAreaNode.customizationTarget;
let areaIsPanel = gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL;
if (!container) {
throw new Error("Expected area " + aArea
+ " to have a customizationTarget attribute.");
}
// Restore nav-bar visibility since it may have been hidden
// through a migration path (bug 938980) or an add-on.
if (aArea == CustomizableUI.AREA_NAVBAR) {
aAreaNode.collapsed = false;
}
this.beginBatchUpdate();
try {
let currentNode = container.firstChild;
let placementsToRemove = new Set();
for (let id of aPlacements) {
while (currentNode && currentNode.getAttribute("skipintoolbarset") == "true") {
currentNode = currentNode.nextSibling;
}
if (currentNode && currentNode.id == id) {
currentNode = currentNode.nextSibling;
continue;
}
if (this.isSpecialWidget(id) && areaIsPanel) {
placementsToRemove.add(id);
continue;
}
let [provider, node] = this.getWidgetNode(id, window);
if (!node) {
log.debug("Unknown widget: " + id);
continue;
}
let widget = null;
// If the placements have items in them which are (now) no longer removable,
// we shouldn't be moving them:
if (provider == CustomizableUI.PROVIDER_API) {
widget = gPalette.get(id);
if (!widget.removable && aArea != widget.defaultArea) {
placementsToRemove.add(id);
continue;
}
} else if (provider == CustomizableUI.PROVIDER_XUL &&
node.parentNode != container && !this.isWidgetRemovable(node)) {
placementsToRemove.add(id);
continue;
} // Special widgets are always removable, so no need to check them
if (inPrivateWindow && widget && !widget.showInPrivateBrowsing) {
continue;
}
this.ensureButtonContextMenu(node, aAreaNode);
if (node.localName == "toolbarbutton") {
if (areaIsPanel) {
node.setAttribute("wrap", "true");
} else {
node.removeAttribute("wrap");
}
}
// This needs updating in case we're resetting / undoing a reset.
if (widget) {
widget.currentArea = aArea;
}
this.insertWidgetBefore(node, currentNode, container, aArea);
if (gResetting) {
this.notifyListeners("onWidgetReset", node, container);
} else if (gUndoResetting) {
this.notifyListeners("onWidgetUndoMove", node, container);
}
}
if (currentNode) {
let palette = aAreaNode.toolbox ? aAreaNode.toolbox.palette : null;
let limit = currentNode.previousSibling;
let node = container.lastChild;
while (node && node != limit) {
let previousSibling = node.previousSibling;
// Nodes opt-in to removability. If they're removable, and we haven't
// seen them in the placements array, then we toss them into the palette
// if one exists. If no palette exists, we just remove the node. If the
// node is not removable, we leave it where it is. However, we can only
// safely touch elements that have an ID - both because we depend on
// IDs, and because such elements are not intended to be widgets
// (eg, titlebar-placeholder elements).
if (node.id && node.getAttribute("skipintoolbarset") != "true") {
if (this.isWidgetRemovable(node)) {
if (palette && !this.isSpecialWidget(node.id)) {
palette.appendChild(node);
this.removeLocationAttributes(node);
} else {
container.removeChild(node);
}
} else {
node.setAttribute("removable", false);
log.debug("Adding non-removable widget to placements of " + aArea + ": " +
node.id);
gPlacements.get(aArea).push(node.id);
gDirty = true;
}
}
node = previousSibling;
}
}
// If there are placements in here which aren't removable from their original area,
// we remove them from this area's placement array. They will (have) be(en) added
// to their original area's placements array in the block above this one.
if (placementsToRemove.size) {
let placementAry = gPlacements.get(aArea);
for (let id of placementsToRemove) {
let index = placementAry.indexOf(id);
placementAry.splice(index, 1);
}
}
if (gResetting) {
this.notifyListeners("onAreaReset", aArea, container);
}
} finally {
this.endBatchUpdate();
}
},
addPanelCloseListeners: function(aPanel) {
gELS.addSystemEventListener(aPanel, "click", this, false);
gELS.addSystemEventListener(aPanel, "keypress", this, false);
let win = aPanel.ownerGlobal;
if (!gPanelsForWindow.has(win)) {
gPanelsForWindow.set(win, new Set());
}
gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel));
},
removePanelCloseListeners: function(aPanel) {
gELS.removeSystemEventListener(aPanel, "click", this, false);
gELS.removeSystemEventListener(aPanel, "keypress", this, false);
let win = aPanel.ownerGlobal;
let panels = gPanelsForWindow.get(win);
if (panels) {
panels.delete(this._getPanelForNode(aPanel));
}
},
ensureButtonContextMenu: function(aNode, aAreaNode) {
const kPanelItemContextMenu = "customizationPanelItemContextMenu";
let currentContextMenu = aNode.getAttribute("context") ||
aNode.getAttribute("contextmenu");
let place = CustomizableUI.getPlaceForItem(aAreaNode);
let contextMenuForPlace = place == "panel" ?
kPanelItemContextMenu :
null;
if (contextMenuForPlace && !currentContextMenu) {
aNode.setAttribute("context", contextMenuForPlace);
} else if (currentContextMenu == kPanelItemContextMenu &&
contextMenuForPlace != kPanelItemContextMenu) {
aNode.removeAttribute("context");
aNode.removeAttribute("contextmenu");
}
},
getWidgetProvider: function(aWidgetId) {
if (this.isSpecialWidget(aWidgetId)) {
return CustomizableUI.PROVIDER_SPECIAL;
}
if (gPalette.has(aWidgetId)) {
return CustomizableUI.PROVIDER_API;
}
// If this was an API widget that was destroyed, return null:
if (gSeenWidgets.has(aWidgetId)) {
return null;
}
// We fall back to the XUL provider, but we don't know for sure (at this
// point) whether it exists there either. So the API is technically lying.
// Ideally, it would be able to return an error value (or throw an
// exception) if it really didn't exist. Our code calling this function
// handles that fine, but this is a public API.
return CustomizableUI.PROVIDER_XUL;
},
getWidgetNode: function(aWidgetId, aWindow) {
let document = aWindow.document;
if (this.isSpecialWidget(aWidgetId)) {
let widgetNode = document.getElementById(aWidgetId) ||
this.createSpecialWidget(aWidgetId, document);
return [ CustomizableUI.PROVIDER_SPECIAL, widgetNode];
}
let widget = gPalette.get(aWidgetId);
if (widget) {
// If we have an instance of this widget already, just use that.
if (widget.instances.has(document)) {
log.debug("An instance of widget " + aWidgetId + " already exists in this "
+ "document. Reusing.");
return [ CustomizableUI.PROVIDER_API,
widget.instances.get(document) ];
}
return [ CustomizableUI.PROVIDER_API,
this.buildWidget(document, widget) ];
}
log.debug("Searching for " + aWidgetId + " in toolbox.");
let node = this.findWidgetInWindow(aWidgetId, aWindow);
if (node) {
return [ CustomizableUI.PROVIDER_XUL, node ];
}
log.debug("No node for " + aWidgetId + " found.");
return [null, null];
},
registerMenuPanel: function(aPanelContents) {
if (gBuildAreas.has(CustomizableUI.AREA_PANEL) &&
gBuildAreas.get(CustomizableUI.AREA_PANEL).has(aPanelContents)) {
return;
}
let document = aPanelContents.ownerDocument;
aPanelContents.toolbox = document.getElementById("navigator-toolbox");
aPanelContents.customizationTarget = aPanelContents;
this.addPanelCloseListeners(this._getPanelForNode(aPanelContents));
let placements = gPlacements.get(CustomizableUI.AREA_PANEL);
this.buildArea(CustomizableUI.AREA_PANEL, placements, aPanelContents);
this.notifyListeners("onAreaNodeRegistered", CustomizableUI.AREA_PANEL, aPanelContents);
for (let child of aPanelContents.children) {
if (child.localName != "toolbarbutton") {
if (child.localName == "toolbaritem") {
this.ensureButtonContextMenu(child, aPanelContents);
}
continue;
}
this.ensureButtonContextMenu(child, aPanelContents);
child.setAttribute("wrap", "true");
}
this.registerBuildArea(CustomizableUI.AREA_PANEL, aPanelContents);
},
onWidgetAdded: function(aWidgetId, aArea, aPosition) {
this.insertNode(aWidgetId, aArea, aPosition, true);
if (!gResetting) {
this._clearPreviousUIState();
}
},
onWidgetRemoved: function(aWidgetId, aArea) {
let areaNodes = gBuildAreas.get(aArea);
if (!areaNodes) {
return;
}
let area = gAreas.get(aArea);
let isToolbar = area.get("type") == CustomizableUI.TYPE_TOOLBAR;
let isOverflowable = isToolbar && area.get("overflowable");
let showInPrivateBrowsing = gPalette.has(aWidgetId)
? gPalette.get(aWidgetId).showInPrivateBrowsing
: true;
for (let areaNode of areaNodes) {
let window = areaNode.ownerGlobal;
if (!showInPrivateBrowsing &&
PrivateBrowsingUtils.isWindowPrivate(window)) {
continue;
}
let container = areaNode.customizationTarget;
let widgetNode = window.document.getElementById(aWidgetId);
if (widgetNode && isOverflowable) {
container = areaNode.overflowable.getContainerFor(widgetNode);
}
if (!widgetNode || !container.contains(widgetNode)) {
log.info("Widget " + aWidgetId + " not found, unable to remove from " + aArea);
continue;
}
this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null, container, true);
// We remove location attributes here to make sure they're gone too when a
// widget is removed from a toolbar to the palette. See bug 930950.
this.removeLocationAttributes(widgetNode);
// We also need to remove the panel context menu if it's there:
this.ensureButtonContextMenu(widgetNode);
widgetNode.removeAttribute("wrap");
if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) {
container.removeChild(widgetNode);
} else {
areaNode.toolbox.palette.appendChild(widgetNode);
}
this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null, container, true);
if (isToolbar) {
areaNode.setAttribute("currentset", gPlacements.get(aArea).join(','));
}
let windowCache = gSingleWrapperCache.get(window);
if (windowCache) {
windowCache.delete(aWidgetId);
}
}
if (!gResetting) {
this._clearPreviousUIState();
}
},
onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) {
this.insertNode(aWidgetId, aArea, aNewPosition);
if (!gResetting) {
this._clearPreviousUIState();
}
},
onCustomizeEnd: function(aWindow) {
this._clearPreviousUIState();
},
registerBuildArea: function(aArea, aNode) {
// We ensure that the window is registered to have its customization data
// cleaned up when unloading.
let window = aNode.ownerGlobal;
if (window.closed) {
return;
}
this.registerBuildWindow(window);
// Also register this build area's toolbox.
if (aNode.toolbox) {
gBuildWindows.get(window).add(aNode.toolbox);
}
if (!gBuildAreas.has(aArea)) {
gBuildAreas.set(aArea, new Set());
}
gBuildAreas.get(aArea).add(aNode);
// Give a class to all customize targets to be used for styling in Customize Mode
let customizableNode = this.getCustomizeTargetForArea(aArea, window);
customizableNode.classList.add("customization-target");
},
registerBuildWindow: function(aWindow) {
if (!gBuildWindows.has(aWindow)) {
gBuildWindows.set(aWindow, new Set());
aWindow.addEventListener("unload", this);
aWindow.addEventListener("command", this, true);
this.notifyListeners("onWindowOpened", aWindow);
}
},
unregisterBuildWindow: function(aWindow) {
aWindow.removeEventListener("unload", this);
aWindow.removeEventListener("command", this, true);
gPanelsForWindow.delete(aWindow);
gBuildWindows.delete(aWindow);
gSingleWrapperCache.delete(aWindow);
let document = aWindow.document;
for (let [areaId, areaNodes] of gBuildAreas) {
let areaProperties = gAreas.get(areaId);
for (let node of areaNodes) {
if (node.ownerDocument == document) {
this.notifyListeners("onAreaNodeUnregistered", areaId, node.customizationTarget,
CustomizableUI.REASON_WINDOW_CLOSED);
if (areaProperties.has("overflowable")) {
node.overflowable.uninit();
node.overflowable = null;
}
areaNodes.delete(node);
}
}
}
for (let [, widget] of gPalette) {
widget.instances.delete(document);
this.notifyListeners("onWidgetInstanceRemoved", widget.id, document);
}
for (let [, areaMap] of gPendingBuildAreas) {
let toDelete = [];
for (let [areaNode, ] of areaMap) {
if (areaNode.ownerDocument == document) {
toDelete.push(areaNode);
}
}
for (let areaNode of toDelete) {
areaMap.delete(areaNode);
}
}
this.notifyListeners("onWindowClosed", aWindow);
},
setLocationAttributes: function(aNode, aArea) {
let props = gAreas.get(aArea);
if (!props) {
throw new Error("Expected area " + aArea + " to have a properties Map " +
"associated with it.");
}
aNode.setAttribute("cui-areatype", props.get("type") || "");
let anchor = props.get("anchor");
if (anchor) {
aNode.setAttribute("cui-anchorid", anchor);
} else {
aNode.removeAttribute("cui-anchorid");
}
},
removeLocationAttributes: function(aNode) {
aNode.removeAttribute("cui-areatype");
aNode.removeAttribute("cui-anchorid");
},
insertNode: function(aWidgetId, aArea, aPosition, isNew) {
let areaNodes = gBuildAreas.get(aArea);
if (!areaNodes) {
return;
}
let placements = gPlacements.get(aArea);
if (!placements) {
log.error("Could not find any placements for " + aArea +
" when moving a widget.");
return;
}
// Go through each of the nodes associated with this area and move the
// widget to the requested location.
for (let areaNode of areaNodes) {
this.insertNodeInWindow(aWidgetId, areaNode, isNew);
}
},
insertNodeInWindow: function(aWidgetId, aAreaNode, isNew) {
let window = aAreaNode.ownerGlobal;
let showInPrivateBrowsing = gPalette.has(aWidgetId)
? gPalette.get(aWidgetId).showInPrivateBrowsing
: true;
if (!showInPrivateBrowsing && PrivateBrowsingUtils.isWindowPrivate(window)) {
return;
}
let [, widgetNode] = this.getWidgetNode(aWidgetId, window);
if (!widgetNode) {
log.error("Widget '" + aWidgetId + "' not found, unable to move");
return;
}
let areaId = aAreaNode.id;
if (isNew) {
this.ensureButtonContextMenu(widgetNode, aAreaNode);
if (widgetNode.localName == "toolbarbutton" && areaId == CustomizableUI.AREA_PANEL) {
widgetNode.setAttribute("wrap", "true");
}
}
let [insertionContainer, nextNode] = this.findInsertionPoints(widgetNode, aAreaNode);
this.insertWidgetBefore(widgetNode, nextNode, insertionContainer, areaId);
if (gAreas.get(areaId).get("type") == CustomizableUI.TYPE_TOOLBAR) {
aAreaNode.setAttribute("currentset", gPlacements.get(areaId).join(','));
}
},
findInsertionPoints: function(aNode, aAreaNode) {
let areaId = aAreaNode.id;
let props = gAreas.get(areaId);
// For overflowable toolbars, rely on them (because the work is more complicated):
if (props.get("type") == CustomizableUI.TYPE_TOOLBAR && props.get("overflowable")) {
return aAreaNode.overflowable.findOverflowedInsertionPoints(aNode);
}
let container = aAreaNode.customizationTarget;
let placements = gPlacements.get(areaId);
let nodeIndex = placements.indexOf(aNode.id);
while (++nodeIndex < placements.length) {
let nextNodeId = placements[nodeIndex];
let nextNode = container.getElementsByAttribute("id", nextNodeId).item(0);
if (nextNode) {
return [container, nextNode];
}
}
return [container, null];
},
insertWidgetBefore: function(aNode, aNextNode, aContainer, aArea) {
this.notifyListeners("onWidgetBeforeDOMChange", aNode, aNextNode, aContainer);
this.setLocationAttributes(aNode, aArea);
aContainer.insertBefore(aNode, aNextNode);
this.notifyListeners("onWidgetAfterDOMChange", aNode, aNextNode, aContainer);
},
handleEvent: function(aEvent) {
switch (aEvent.type) {
case "command":
if (!this._originalEventInPanel(aEvent)) {
break;
}
aEvent = aEvent.sourceEvent;
// Fall through
case "click":
case "keypress":
this.maybeAutoHidePanel(aEvent);
break;
case "unload":
this.unregisterBuildWindow(aEvent.currentTarget);
break;
}
},
_originalEventInPanel: function(aEvent) {
let e = aEvent.sourceEvent;
if (!e) {
return false;
}
let node = this._getPanelForNode(e.target);
if (!node) {
return false;
}
let win = e.view;
let panels = gPanelsForWindow.get(win);
return !!panels && panels.has(node);
},
isSpecialWidget: function(aId) {
return (aId.startsWith(kSpecialWidgetPfx) ||
aId.startsWith("separator") ||
aId.startsWith("spring") ||
aId.startsWith("spacer"));
},
ensureSpecialWidgetId: function(aId) {
let nodeType = aId.match(/spring|spacer|separator/)[0];
// If the ID we were passed isn't a generated one, generate one now:
if (nodeType == aId) {
// Ids are differentiated through a unique count suffix.
return kSpecialWidgetPfx + aId + (++gNewElementCount);
}
return aId;
},
createSpecialWidget: function(aId, aDocument) {
let nodeName = "toolbar" + aId.match(/spring|spacer|separator/)[0];
let node = aDocument.createElementNS(kNSXUL, nodeName);
node.id = this.ensureSpecialWidgetId(aId);
if (nodeName == "toolbarspring") {
node.flex = 1;
}
return node;
},
/* Find a XUL-provided widget in a window. Don't try to use this
* for an API-provided widget or a special widget.
*/
findWidgetInWindow: function(aId, aWindow) {
if (!gBuildWindows.has(aWindow)) {
throw new Error("Build window not registered");
}
if (!aId) {
log.error("findWidgetInWindow was passed an empty string.");
return null;
}
let document = aWindow.document;
// look for a node with the same id, as the node may be
// in a different toolbar.
let node = document.getElementById(aId);
if (node) {
let parent = node.parentNode;
while (parent && !(parent.customizationTarget ||
parent == aWindow.gNavToolbox.palette)) {
parent = parent.parentNode;
}
if (parent) {
let nodeInArea = node.parentNode.localName == "toolbarpaletteitem" ?
node.parentNode : node;
// Check if we're in a customization target, or in the palette:
if ((parent.customizationTarget == nodeInArea.parentNode &&
gBuildWindows.get(aWindow).has(parent.toolbox)) ||
aWindow.gNavToolbox.palette == nodeInArea.parentNode) {
// Normalize the removable attribute. For backwards compat, if
// the widget is not located in a toolbox palette then absence
// of the "removable" attribute means it is not removable.
if (!node.hasAttribute("removable")) {
// If we first see this in customization mode, it may be in the
// customization palette instead of the toolbox palette.
node.setAttribute("removable", !parent.customizationTarget);
}
return node;
}
}
}
let toolboxes = gBuildWindows.get(aWindow);
for (let toolbox of toolboxes) {
if (toolbox.palette) {
// Attempt to locate a node with a matching ID within
// the palette.
let node = toolbox.palette.getElementsByAttribute("id", aId)[0];
if (node) {
// Normalize the removable attribute. For backwards compat, this
// is optional if the widget is located in the toolbox palette,
// and defaults to *true*, unlike if it was located elsewhere.
if (!node.hasAttribute("removable")) {
node.setAttribute("removable", true);
}
return node;
}
}
}
return null;
},
buildWidget: function(aDocument, aWidget) {
if (aDocument.documentURI != kExpectedWindowURL) {
throw new Error("buildWidget was called for a non-browser window!");
}
if (typeof aWidget == "string") {
aWidget = gPalette.get(aWidget);
}
if (!aWidget) {
throw new Error("buildWidget was passed a non-widget to build.");
}
log.debug("Building " + aWidget.id + " of type " + aWidget.type);
let node;
if (aWidget.type == "custom") {
if (aWidget.onBuild) {
node = aWidget.onBuild(aDocument);
}
if (!node || !(node instanceof aDocument.defaultView.XULElement))
log.error("Custom widget with id " + aWidget.id + " does not return a valid node");
}
else {
if (aWidget.onBeforeCreated) {
aWidget.onBeforeCreated(aDocument);
}
node = aDocument.createElementNS(kNSXUL, "toolbarbutton");
node.setAttribute("id", aWidget.id);
node.setAttribute("widget-id", aWidget.id);
node.setAttribute("widget-type", aWidget.type);
if (aWidget.disabled) {
node.setAttribute("disabled", true);
}
node.setAttribute("removable", aWidget.removable);
node.setAttribute("overflows", aWidget.overflows);
if (aWidget.tabSpecific) {
node.setAttribute("tabspecific", aWidget.tabSpecific);
}
node.setAttribute("label", this.getLocalizedProperty(aWidget, "label"));
let additionalTooltipArguments = [];
if (aWidget.shortcutId) {
let keyEl = aDocument.getElementById(aWidget.shortcutId);
if (keyEl) {
additionalTooltipArguments.push(ShortcutUtils.prettifyShortcut(keyEl));
} else {
log.error("Key element with id '" + aWidget.shortcutId + "' for widget '" + aWidget.id +
"' not found!");
}
}
let tooltip = this.getLocalizedProperty(aWidget, "tooltiptext", additionalTooltipArguments);
if (tooltip) {
node.setAttribute("tooltiptext", tooltip);
}
node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional");
let commandHandler = this.handleWidgetCommand.bind(this, aWidget, node);
node.addEventListener("command", commandHandler, false);
let clickHandler = this.handleWidgetClick.bind(this, aWidget, node);
node.addEventListener("click", clickHandler, false);
// If the widget has a view, and has view showing / hiding listeners,
// hook those up to this widget.
if (aWidget.type == "view") {
log.debug("Widget " + aWidget.id + " has a view. Auto-registering event handlers.");
let viewNode = aDocument.getElementById(aWidget.viewId);
if (viewNode) {
// PanelUI relies on the .PanelUI-subView class to be able to show only
// one sub-view at a time.
viewNode.classList.add("PanelUI-subView");
for (let eventName of kSubviewEvents) {
let handler = "on" + eventName;
if (typeof aWidget[handler] == "function") {
viewNode.addEventListener(eventName, aWidget[handler], false);
}
}
log.debug("Widget " + aWidget.id + " showing and hiding event handlers set.");
} else {
log.error("Could not find the view node with id: " + aWidget.viewId +
", for widget: " + aWidget.id + ".");
}
}
if (aWidget.onCreated) {
aWidget.onCreated(node);
}
}
aWidget.instances.set(aDocument, node);
return node;
},
getLocalizedProperty: function(aWidget, aProp, aFormatArgs, aDef) {
const kReqStringProps = ["label"];
if (typeof aWidget == "string") {
aWidget = gPalette.get(aWidget);
}
if (!aWidget) {
throw new Error("getLocalizedProperty was passed a non-widget to work with.");
}
let def, name;
// Let widgets pass their own string identifiers or strings, so that
// we can use strings which aren't the default (in case string ids change)
// and so that non-builtin-widgets can also provide labels, tooltips, etc.
if (aWidget[aProp] != null) {
name = aWidget[aProp];
// By using this as the default, if a widget provides a full string rather
// than a string ID for localization, we will fall back to that string
// and return that.
def = aDef || name;
} else {
name = aWidget.id + "." + aProp;
def = aDef || "";
}
try {
if (Array.isArray(aFormatArgs) && aFormatArgs.length) {
return gWidgetsBundle.formatStringFromName(name, aFormatArgs,
aFormatArgs.length) || def;
}
return gWidgetsBundle.GetStringFromName(name) || def;
} catch (ex) {
// If an empty string was explicitly passed, treat it as an actual
// value rather than a missing property.
if (!def && (name != "" || kReqStringProps.includes(aProp))) {
log.error("Could not localize property '" + name + "'.");
}
}
return def;
},
addShortcut: function(aShortcutNode, aTargetNode) {
if (!aTargetNode)
aTargetNode = aShortcutNode;
let document = aShortcutNode.ownerDocument;
// Detect if we've already been here before.
if (!aTargetNode || aTargetNode.hasAttribute("shortcut"))
return;
let shortcutId = aShortcutNode.getAttribute("key");
let shortcut;
if (shortcutId) {
shortcut = document.getElementById(shortcutId);
} else {
let commandId = aShortcutNode.getAttribute("command");
if (commandId)
shortcut = ShortcutUtils.findShortcut(document.getElementById(commandId));
}
if (!shortcut) {
return;
}
aTargetNode.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(shortcut));
},
handleWidgetCommand: function(aWidget, aNode, aEvent) {
log.debug("handleWidgetCommand");
if (aWidget.type == "button") {
if (aWidget.onCommand) {
try {
aWidget.onCommand.call(null, aEvent);
} catch (e) {
log.error(e);
}
} else {
// XXXunf Need to think this through more, and formalize.
Services.obs.notifyObservers(aNode,
"customizedui-widget-command",
aWidget.id);
}
} else if (aWidget.type == "view") {
let ownerWindow = aNode.ownerGlobal;
let area = this.getPlacementOfWidget(aNode.id).area;
let anchor = aNode;
if (area != CustomizableUI.AREA_PANEL) {
let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow);
if (wrapper && wrapper.anchor) {
this.hidePanelForNode(aNode);
anchor = wrapper.anchor;
}
}
ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, area);
}
},
handleWidgetClick: function(aWidget, aNode, aEvent) {
log.debug("handleWidgetClick");
if (aWidget.onClick) {
try {
aWidget.onClick.call(null, aEvent);
} catch (e) {
Cu.reportError(e);
}
} else {
// XXXunf Need to think this through more, and formalize.
Services.obs.notifyObservers(aNode, "customizedui-widget-click", aWidget.id);
}
},
_getPanelForNode: function(aNode) {
let panel = aNode;
while (panel && panel.localName != "panel")
panel = panel.parentNode;
return panel;
},
/*
* If people put things in the panel which need more than single-click interaction,
* we don't want to close it. Right now we check for text inputs and menu buttons.
* We also check for being outside of any toolbaritem/toolbarbutton, ie on a blank
* part of the menu.
*/
_isOnInteractiveElement: function(aEvent) {
function getMenuPopupForDescendant(aNode) {
let lastPopup = null;
while (aNode && aNode.parentNode &&
aNode.parentNode.localName.startsWith("menu")) {
lastPopup = aNode.localName == "menupopup" ? aNode : lastPopup;
aNode = aNode.parentNode;
}
return lastPopup;
}
let target = aEvent.originalTarget;
let panel = this._getPanelForNode(aEvent.currentTarget);
// This can happen in e.g. customize mode. If there's no panel,
// there's clearly nothing for us to close; pretend we're interactive.
if (!panel) {
return true;
}
// We keep track of:
// whether we're in an input container (text field)
let inInput = false;
// whether we're in a popup/context menu
let inMenu = false;
// whether we're in a toolbarbutton/toolbaritem
let inItem = false;
// whether the current menuitem has a valid closemenu attribute
let menuitemCloseMenu = "auto";
// whether the toolbarbutton/item has a valid closemenu attribute.
let closemenu = "auto";
// While keeping track of that, we go from the original target back up,
// to the panel if we have to. We bail as soon as we find an input,
// a toolbarbutton/item, or the panel:
while (true && target) {
// Skip out of iframes etc:
if (target.nodeType == target.DOCUMENT_NODE) {
if (!target.defaultView) {
// Err, we're done.
break;
}
// Cue some voodoo
target = target.defaultView.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIWebNavigation)
.QueryInterface(Ci.nsIDocShell)
.chromeEventHandler;
if (!target) {
break;
}
}
let tagName = target.localName;
inInput = tagName == "input" || tagName == "textbox";
inItem = tagName == "toolbaritem" || tagName == "toolbarbutton";
let isMenuItem = tagName == "menuitem";
inMenu = inMenu || isMenuItem;
if (inItem && target.hasAttribute("closemenu")) {
let closemenuVal = target.getAttribute("closemenu");
closemenu = (closemenuVal == "single" || closemenuVal == "none") ?
closemenuVal : "auto";
}
if (isMenuItem && target.hasAttribute("closemenu")) {
let closemenuVal = target.getAttribute("closemenu");
menuitemCloseMenu = (closemenuVal == "single" || closemenuVal == "none") ?
closemenuVal : "auto";
}
// Break out of the loop immediately for disabled items, as we need to
// keep the menu open in that case.
if (target.getAttribute("disabled") == "true") {
return true;
}
// This isn't in the loop condition because we want to break before
// changing |target| if any of these conditions are true
if (inInput || inItem || target == panel) {
break;
}
// We need specific code for popups: the item on which they were invoked
// isn't necessarily in their parentNode chain:
if (isMenuItem) {
let topmostMenuPopup = getMenuPopupForDescendant(target);
target = (topmostMenuPopup && topmostMenuPopup.triggerNode) ||
target.parentNode;
} else {
target = target.parentNode;
}
}
// If the user clicked a menu item...
if (inMenu) {
// We care if we're in an input also,
// or if the user specified closemenu!="auto":
if (inInput || menuitemCloseMenu != "auto") {
return true;
}
// Otherwise, we're probably fine to close the panel
return false;
}
// If we're not in a menu, and we *are* in a type="menu" toolbarbutton,
// we'll now interact with the menu
if (inItem && target.getAttribute("type") == "menu") {
return true;
}
// If we're not in a menu, and we *are* in a type="menu-button" toolbarbutton,
// it depends whether we're in the dropmarker or the 'real' button:
if (inItem && target.getAttribute("type") == "menu-button") {
// 'real' button (which has a single action):
if (target.getAttribute("anonid") == "button") {
return closemenu != "none";
}
// otherwise, this is the outer button, and the user will now
// interact with the menu:
return true;
}
return inInput || !inItem;
},
hidePanelForNode: function(aNode) {
let panel = this._getPanelForNode(aNode);
if (panel) {
panel.hidePopup();
}
},
maybeAutoHidePanel: function(aEvent) {
if (aEvent.type == "keypress") {
if (aEvent.keyCode != aEvent.DOM_VK_RETURN) {
return;
}
// If the user hit enter/return, we don't check preventDefault - it makes sense
// that this was prevented, but we probably still want to close the panel.
// If consumers don't want this to happen, they should specify the closemenu
// attribute.
} else if (aEvent.type != "command") { // mouse events:
if (aEvent.defaultPrevented || aEvent.button != 0) {
return;
}
let isInteractive = this._isOnInteractiveElement(aEvent);
log.debug("maybeAutoHidePanel: interactive ? " + isInteractive);
if (isInteractive) {
return;
}
}
// We can't use event.target because we might have passed a panelview
// anonymous content boundary as well, and so target points to the
// panelmultiview in that case. Unfortunately, this means we get
// anonymous child nodes instead of the real ones, so looking for the
// 'stoooop, don't close me' attributes is more involved.
let target = aEvent.originalTarget;
let closemenu = "auto";
let widgetType = "button";
while (target.parentNode && target.localName != "panel") {
closemenu = target.getAttribute("closemenu");
widgetType = target.getAttribute("widget-type");
if (closemenu == "none" || closemenu == "single" ||
widgetType == "view") {
break;
}
target = target.parentNode;
}
if (closemenu == "none" || widgetType == "view") {
return;
}
if (closemenu == "single") {
let panel = this._getPanelForNode(target);
let multiview = panel.querySelector("panelmultiview");
if (multiview.showingSubView) {
multiview.showMainView();
return;
}
}
// If we get here, we can actually hide the popup:
this.hidePanelForNode(aEvent.target);
},
getUnusedWidgets: function(aWindowPalette) {
let window = aWindowPalette.ownerGlobal;
let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window);
// We use a Set because there can be overlap between the widgets in
// gPalette and the items in the palette, especially after the first
// customization, since programmatically generated widgets will remain
// in the toolbox palette.
let widgets = new Set();
// It's possible that some widgets have been defined programmatically and
// have not been overlayed into the palette. We can find those inside
// gPalette.
for (let [id, widget] of gPalette) {
if (!widget.currentArea) {
if (widget.showInPrivateBrowsing || !isWindowPrivate) {
widgets.add(id);
}
}
}
log.debug("Iterating the actual nodes of the window palette");
for (let node of aWindowPalette.children) {
log.debug("In palette children: " + node.id);
if (node.id && !this.getPlacementOfWidget(node.id)) {
widgets.add(node.id);
}
}
return [...widgets];
},
getPlacementOfWidget: function(aWidgetId, aOnlyRegistered, aDeadAreas) {
if (aOnlyRegistered && !this.widgetExists(aWidgetId)) {
return null;
}
for (let [area, placements] of gPlacements) {
if (!gAreas.has(area) && !aDeadAreas) {
continue;
}
let index = placements.indexOf(aWidgetId);
if (index != -1) {
return { area: area, position: index };
}
}
return null;
},
widgetExists: function(aWidgetId) {
if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) {
return true;
}
// Destroyed API widgets are in gSeenWidgets, but not in gPalette:
if (gSeenWidgets.has(aWidgetId)) {
return false;
}
// We're assuming XUL widgets always exist, as it's much harder to check,
// and checking would be much more error prone.
return true;
},
addWidgetToArea: function(aWidgetId, aArea, aPosition, aInitialAdd) {
if (!gAreas.has(aArea)) {
throw new Error("Unknown customization area: " + aArea);
}
// Hack: don't want special widgets in the panel (need to check here as well
// as in canWidgetMoveToArea because the menu panel is lazy):
if (gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL &&
this.isSpecialWidget(aWidgetId)) {
return;
}
// If this is a lazy area that hasn't been restored yet, we can't yet modify
// it - would would at least like to add to it. So we keep track of it in
// gFuturePlacements, and use that to add it when restoring the area. We
// throw away aPosition though, as that can only be bogus if the area hasn't
// yet been restorted (caller can't possibly know where its putting the
// widget in relation to other widgets).
if (this.isAreaLazy(aArea)) {
gFuturePlacements.get(aArea).add(aWidgetId);
return;
}
if (this.isSpecialWidget(aWidgetId)) {
aWidgetId = this.ensureSpecialWidgetId(aWidgetId);
}
let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
if (oldPlacement && oldPlacement.area == aArea) {
this.moveWidgetWithinArea(aWidgetId, aPosition);
return;
}
// Do nothing if the widget is not allowed to move to the target area.
if (!this.canWidgetMoveToArea(aWidgetId, aArea)) {
return;
}
if (oldPlacement) {
this.removeWidgetFromArea(aWidgetId);
}
if (!gPlacements.has(aArea)) {
gPlacements.set(aArea, [aWidgetId]);
aPosition = 0;
} else {
let placements = gPlacements.get(aArea);
if (typeof aPosition != "number") {
aPosition = placements.length;
}
if (aPosition < 0) {
aPosition = 0;
}
placements.splice(aPosition, 0, aWidgetId);
}
let widget = gPalette.get(aWidgetId);
if (widget) {
widget.currentArea = aArea;
widget.currentPosition = aPosition;
}
// We initially set placements with addWidgetToArea, so in that case
// we don't consider the area "dirtied".
if (!aInitialAdd) {
gDirtyAreaCache.add(aArea);
}
gDirty = true;
this.saveState();
this.notifyListeners("onWidgetAdded", aWidgetId, aArea, aPosition);
},
removeWidgetFromArea: function(aWidgetId) {
let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true);
if (!oldPlacement) {
return;
}
if (!this.isWidgetRemovable(aWidgetId)) {
return;
}
let placements = gPlacements.get(oldPlacement.area);
let position = placements.indexOf(aWidgetId);
if (position != -1) {
placements.splice(position, 1);
}
let widget = gPalette.get(aWidgetId);
if (widget) {
widget.currentArea = null;
widget.currentPosition = null;
}
gDirty = true;
this.saveState();
gDirtyAreaCache.add(oldPlacement.area);
this.notifyListeners("onWidgetRemoved", aWidgetId, oldPlacement.area);
},
moveWidgetWithinArea: function(aWidgetId, aPosition) {
let oldPlacement = this.getPlacementOfWidget(aWidgetId);
if (!oldPlacement) {
return;
}
let placements = gPlacements.get(oldPlacement.area);
if (typeof aPosition != "number") {
aPosition = placements.length;
} else if (aPosition < 0) {
aPosition = 0;
} else if (aPosition > placements.length) {
aPosition = placements.length;
}
let widget = gPalette.get(aWidgetId);
if (widget) {
widget.currentPosition = aPosition;
widget.currentArea = oldPlacement.area;
}
if (aPosition == oldPlacement.position) {
return;
}
placements.splice(oldPlacement.position, 1);
// If we just removed the item from *before* where it is now added,
// we need to compensate the position offset for that:
if (oldPlacement.position < aPosition) {
aPosition--;
}
placements.splice(aPosition, 0, aWidgetId);
gDirty = true;
gDirtyAreaCache.add(oldPlacement.area);
this.saveState();
this.notifyListeners("onWidgetMoved", aWidgetId, oldPlacement.area,
oldPlacement.position, aPosition);
},
// Note that this does not populate gPlacements, which is done lazily so that
// the legacy state can be migrated, which is only available once a browser
// window is openned.
// The panel area is an exception here, since it has no legacy state and is
// built lazily - and therefore wouldn't otherwise result in restoring its
// state immediately when a browser window opens, which is important for
// other consumers of this API.
loadSavedState: function() {
let state = Services.prefs.getCharPref(kPrefCustomizationState, "");
if (!state) {
log.debug("No saved state found");
// Nothing has been customized, so silently fall back to the defaults.
return;
}
try {
gSavedState = JSON.parse(state);
if (typeof gSavedState != "object" || gSavedState === null) {
throw "Invalid saved state";
}
} catch (e) {
Services.prefs.clearUserPref(kPrefCustomizationState);
gSavedState = {};
log.debug("Error loading saved UI customization state, falling back to defaults.");
}
if (!("placements" in gSavedState)) {
gSavedState.placements = {};
}
if (!("currentVersion" in gSavedState)) {
gSavedState.currentVersion = 0;
}
gSeenWidgets = new Set(gSavedState.seen || []);
gDirtyAreaCache = new Set(gSavedState.dirtyAreaCache || []);
gNewElementCount = gSavedState.newElementCount || 0;
},
restoreStateForArea: function(aArea, aLegacyState) {
let placementsPreexisted = gPlacements.has(aArea);
this.beginBatchUpdate();
try {
gRestoring = true;
let restored = false;
if (placementsPreexisted) {
log.debug("Restoring " + aArea + " from pre-existing placements");
for (let [position, id] of gPlacements.get(aArea).entries()) {
this.moveWidgetWithinArea(id, position);
}
gDirty = false;
restored = true;
} else {
gPlacements.set(aArea, []);
}
if (!restored && gSavedState && aArea in gSavedState.placements) {
log.debug("Restoring " + aArea + " from saved state");
let placements = gSavedState.placements[aArea];
for (let id of placements)
this.addWidgetToArea(id, aArea);
gDirty = false;
restored = true;
}
if (!restored && aLegacyState) {
log.debug("Restoring " + aArea + " from legacy state");
for (let id of aLegacyState)
this.addWidgetToArea(id, aArea);
// Don't override dirty state, to ensure legacy state is saved here and
// therefore only used once.
restored = true;
}
if (!restored) {
log.debug("Restoring " + aArea + " from default state");
let defaults = gAreas.get(aArea).get("defaultPlacements");
if (defaults) {
for (let id of defaults)
this.addWidgetToArea(id, aArea, null, true);
}
gDirty = false;
}
// Finally, add widgets to the area that were added before the it was able
// to be restored. This can occur when add-ons register widgets for a
// lazily-restored area before it's been restored.
if (gFuturePlacements.has(aArea)) {
for (let id of gFuturePlacements.get(aArea))
this.addWidgetToArea(id, aArea);
gFuturePlacements.delete(aArea);
}
log.debug("Placements for " + aArea + ":\n\t" + gPlacements.get(aArea).join("\n\t"));
gRestoring = false;
} finally {
this.endBatchUpdate();
}
},
saveState: function() {
if (gInBatchStack || !gDirty) {
return;
}
// Clone because we want to modify this map:
let state = { placements: new Map(gPlacements),
seen: gSeenWidgets,
dirtyAreaCache: gDirtyAreaCache,
currentVersion: kVersion,
newElementCount: gNewElementCount };
// Merge in previously saved areas if not present in gPlacements.
// This way, state is still persisted for e.g. temporarily disabled
// add-ons - see bug 989338.
if (gSavedState && gSavedState.placements) {
for (let area of Object.keys(gSavedState.placements)) {
if (!state.placements.has(area)) {
let placements = gSavedState.placements[area];
state.placements.set(area, placements);
}
}
}
log.debug("Saving state.");
let serialized = JSON.stringify(state, this.serializerHelper);
log.debug("State saved as: " + serialized);
Services.prefs.setCharPref(kPrefCustomizationState, serialized);
gDirty = false;
},
serializerHelper: function(aKey, aValue) {
if (typeof aValue == "object" && aValue.constructor.name == "Map") {
let result = {};
for (let [mapKey, mapValue] of aValue)
result[mapKey] = mapValue;
return result;
}
if (typeof aValue == "object" && aValue.constructor.name == "Set") {
return [...aValue];
}
return aValue;
},
beginBatchUpdate: function() {
gInBatchStack++;
},
endBatchUpdate: function(aForceDirty) {
gInBatchStack--;
if (aForceDirty === true) {
gDirty = true;
}
if (gInBatchStack == 0) {
this.saveState();
} else if (gInBatchStack < 0) {
throw new Error("The batch editing stack should never reach a negative number.");
}
},
addListener: function(aListener) {
gListeners.add(aListener);
},
removeListener: function(aListener) {
if (aListener == this) {
return;
}
gListeners.delete(aListener);
},
notifyListeners: function(aEvent, ...aArgs) {
if (gRestoring) {
return;
}
for (let listener of gListeners) {
try {
if (typeof listener[aEvent] == "function") {
listener[aEvent].apply(listener, aArgs);
}
} catch (e) {
log.error(e + " -- " + e.fileName + ":" + e.lineNumber);
}
}
},
_dispatchToolboxEventToWindow: function(aEventType, aDetails, aWindow) {
let evt = new aWindow.CustomEvent(aEventType, {
bubbles: true,
cancelable: true,
detail: aDetails
});
aWindow.gNavToolbox.dispatchEvent(evt);
},
dispatchToolboxEvent: function(aEventType, aDetails={}, aWindow=null) {
if (aWindow) {
this._dispatchToolboxEventToWindow(aEventType, aDetails, aWindow);
return;
}
for (let [win, ] of gBuildWindows) {
this._dispatchToolboxEventToWindow(aEventType, aDetails, win);
}
},
createWidget: function(aProperties) {
let widget = this.normalizeWidget(aProperties, CustomizableUI.SOURCE_EXTERNAL);
// XXXunf This should probably throw.
if (!widget) {
log.error("unable to normalize widget");
return undefined;
}
gPalette.set(widget.id, widget);
// Clear our caches:
gGroupWrapperCache.delete(widget.id);
for (let [win, ] of gBuildWindows) {
let cache = gSingleWrapperCache.get(win);
if (cache) {
cache.delete(widget.id);
}
}
this.notifyListeners("onWidgetCreated", widget.id);
if (widget.defaultArea) {
let addToDefaultPlacements = false;
let area = gAreas.get(widget.defaultArea);
if (!CustomizableUI.isBuiltinToolbar(widget.defaultArea) &&
widget.defaultArea != CustomizableUI.AREA_PANEL) {
addToDefaultPlacements = true;
}
if (addToDefaultPlacements) {
if (area.has("defaultPlacements")) {
area.get("defaultPlacements").push(widget.id);
} else {
area.set("defaultPlacements", [widget.id]);
}
}
}
// Look through previously saved state to see if we're restoring a widget.
let seenAreas = new Set();
let widgetMightNeedAutoAdding = true;
for (let [area, ] of gPlacements) {
seenAreas.add(area);
let areaIsRegistered = gAreas.has(area);
let index = gPlacements.get(area).indexOf(widget.id);
if (index != -1) {
widgetMightNeedAutoAdding = false;
if (areaIsRegistered) {
widget.currentArea = area;
widget.currentPosition = index;
}
break;
}
}
// Also look at saved state data directly in areas that haven't yet been
// restored. Can't rely on this for restored areas, as they may have
// changed.
if (widgetMightNeedAutoAdding && gSavedState) {
for (let area of Object.keys(gSavedState.placements)) {
if (seenAreas.has(area)) {
continue;
}
let areaIsRegistered = gAreas.has(area);
let index = gSavedState.placements[area].indexOf(widget.id);
if (index != -1) {
widgetMightNeedAutoAdding = false;
if (areaIsRegistered) {
widget.currentArea = area;
widget.currentPosition = index;
}
break;
}
}
}
// If we're restoring the widget to it's old placement, fire off the
// onWidgetAdded event - our own handler will take care of adding it to
// any build areas.
this.beginBatchUpdate();
try {
if (widget.currentArea) {
this.notifyListeners("onWidgetAdded", widget.id, widget.currentArea,
widget.currentPosition);
} else if (widgetMightNeedAutoAdding) {
let autoAdd = Services.prefs.getBoolPref(kPrefCustomizationAutoAdd, true);
// If the widget doesn't have an existing placement, and it hasn't been
// seen before, then add it to its default area so it can be used.
// If the widget is not removable, we *have* to add it to its default
// area here.
let canBeAutoAdded = autoAdd && !gSeenWidgets.has(widget.id);
if (!widget.currentArea && (!widget.removable || canBeAutoAdded)) {
if (widget.defaultArea) {
if (this.isAreaLazy(widget.defaultArea)) {
gFuturePlacements.get(widget.defaultArea).add(widget.id);
} else {
this.addWidgetToArea(widget.id, widget.defaultArea);
}
}
}
}
} finally {
// Ensure we always have this widget in gSeenWidgets, and save
// state in case this needs to be done here.
gSeenWidgets.add(widget.id);
this.endBatchUpdate(true);
}
this.notifyListeners("onWidgetAfterCreation", widget.id, widget.currentArea);
return widget.id;
},
createBuiltinWidget: function(aData) {
// This should only ever be called on startup, before any windows are
// opened - so we know there's no build areas to handle. Also, builtin
// widgets are expected to be (mostly) static, so shouldn't affect the
// current placement settings.
// This allows a widget to be both built-in by default but also able to be
// destroyed and removed from the area based on criteria that may not be
// available when the widget is created -- for example, because some other
// feature in the browser supersedes the widget.
let conditionalDestroyPromise = aData.conditionalDestroyPromise || null;
delete aData.conditionalDestroyPromise;
let widget = this.normalizeWidget(aData, CustomizableUI.SOURCE_BUILTIN);
if (!widget) {
log.error("Error creating builtin widget: " + aData.id);
return;
}
log.debug("Creating built-in widget with id: " + widget.id);
gPalette.set(widget.id, widget);
if (conditionalDestroyPromise) {
conditionalDestroyPromise.then(shouldDestroy => {
if (shouldDestroy) {
this.destroyWidget(widget.id);
this.removeWidgetFromArea(widget.id);
}
}, err => {
Cu.reportError(err);
});
}
},
// Returns true if the area will eventually lazily restore (but hasn't yet).
isAreaLazy: function(aArea) {
if (gPlacements.has(aArea)) {
return false;
}
return gAreas.get(aArea).has("legacy");
},
// XXXunf Log some warnings here, when the data provided isn't up to scratch.
normalizeWidget: function(aData, aSource) {
let widget = {
implementation: aData,
source: aSource || CustomizableUI.SOURCE_EXTERNAL,
instances: new Map(),
currentArea: null,
removable: true,
overflows: true,
defaultArea: null,
shortcutId: null,
tabSpecific: false,
tooltiptext: null,
showInPrivateBrowsing: true,
_introducedInVersion: -1,
};
if (typeof aData.id != "string" || !/^[a-z0-9-_]{1,}$/i.test(aData.id)) {
log.error("Given an illegal id in normalizeWidget: " + aData.id);
return null;
}
delete widget.implementation.currentArea;
widget.implementation.__defineGetter__("currentArea", () => widget.currentArea);
const kReqStringProps = ["id"];
for (let prop of kReqStringProps) {
if (typeof aData[prop] != "string") {
log.error("Missing required property '" + prop + "' in normalizeWidget: "
+ aData.id);
return null;
}
widget[prop] = aData[prop];
}
const kOptStringProps = ["label", "tooltiptext", "shortcutId"];
for (let prop of kOptStringProps) {
if (typeof aData[prop] == "string") {
widget[prop] = aData[prop];
}
}
const kOptBoolProps = ["removable", "showInPrivateBrowsing", "overflows", "tabSpecific"];
for (let prop of kOptBoolProps) {
if (typeof aData[prop] == "boolean") {
widget[prop] = aData[prop];
}
}
// When we normalize builtin widgets, areas have not yet been registered:
if (aData.defaultArea &&
(aSource == CustomizableUI.SOURCE_BUILTIN || gAreas.has(aData.defaultArea))) {
widget.defaultArea = aData.defaultArea;
} else if (!widget.removable) {
log.error("Widget '" + widget.id + "' is not removable but does not specify " +
"a valid defaultArea. That's not possible; it must specify a " +
"valid defaultArea as well.");
return null;
}
if ("type" in aData && gSupportedWidgetTypes.has(aData.type)) {
widget.type = aData.type;
} else {
widget.type = "button";
}
widget.disabled = aData.disabled === true;
if (aSource == CustomizableUI.SOURCE_BUILTIN) {
widget._introducedInVersion = aData.introducedInVersion || 0;
}
this.wrapWidgetEventHandler("onBeforeCreated", widget);
this.wrapWidgetEventHandler("onClick", widget);
this.wrapWidgetEventHandler("onCreated", widget);
this.wrapWidgetEventHandler("onDestroyed", widget);
if (widget.type == "button") {
widget.onCommand = typeof aData.onCommand == "function" ?
aData.onCommand :
null;
} else if (widget.type == "view") {
if (typeof aData.viewId != "string") {
log.error("Expected a string for widget " + widget.id + " viewId, but got "
+ aData.viewId);
return null;
}
widget.viewId = aData.viewId;
this.wrapWidgetEventHandler("onViewShowing", widget);
this.wrapWidgetEventHandler("onViewHiding", widget);
} else if (widget.type == "custom") {
this.wrapWidgetEventHandler("onBuild", widget);
}
if (gPalette.has(widget.id)) {
return null;
}
return widget;
},
wrapWidgetEventHandler: function(aEventName, aWidget) {
if (typeof aWidget.implementation[aEventName] != "function") {
aWidget[aEventName] = null;
return;
}
aWidget[aEventName] = function(...aArgs) {
// Wrap inside a try...catch to properly log errors, until bug 862627 is
// fixed, which in turn might help bug 503244.
try {
// Don't copy the function to the normalized widget object, instead
// keep it on the original object provided to the API so that
// additional methods can be implemented and used by the event
// handlers.
return aWidget.implementation[aEventName].apply(aWidget.implementation,
aArgs);
} catch (e) {
Cu.reportError(e);
return undefined;
}
};
},
destroyWidget: function(aWidgetId) {
let widget = gPalette.get(aWidgetId);
if (!widget) {
gGroupWrapperCache.delete(aWidgetId);
for (let [window, ] of gBuildWindows) {
let windowCache = gSingleWrapperCache.get(window);
if (windowCache) {
windowCache.delete(aWidgetId);
}
}
return;
}
// Remove it from the default placements of an area if it was added there:
if (widget.defaultArea) {
let area = gAreas.get(widget.defaultArea);
if (area) {
let defaultPlacements = area.get("defaultPlacements");
// We can assume this is present because if a widget has a defaultArea,
// we automatically create a defaultPlacements array for that area.
let widgetIndex = defaultPlacements.indexOf(aWidgetId);
if (widgetIndex != -1) {
defaultPlacements.splice(widgetIndex, 1);
}
}
}
// This will not remove the widget from gPlacements - we want to keep the
// setting so the widget gets put back in it's old position if/when it
// returns.
for (let [window, ] of gBuildWindows) {
<