michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: "use strict"; michael@0: michael@0: this.EXPORTED_SYMBOLS = ["CustomizableUI"]; michael@0: michael@0: const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; michael@0: michael@0: Cu.import("resource://gre/modules/Services.jsm"); michael@0: Cu.import("resource://gre/modules/XPCOMUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PanelWideWidgetTracker", michael@0: "resource:///modules/PanelWideWidgetTracker.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "CustomizableWidgets", michael@0: "resource:///modules/CustomizableWidgets.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", michael@0: "resource://gre/modules/DeferredTask.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", michael@0: "resource://gre/modules/PrivateBrowsingUtils.jsm"); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "Promise", michael@0: "resource://gre/modules/Promise.jsm"); michael@0: XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() { michael@0: const kUrl = "chrome://browser/locale/customizableui/customizableWidgets.properties"; michael@0: return Services.strings.createBundle(kUrl); michael@0: }); michael@0: XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils", michael@0: "resource://gre/modules/ShortcutUtils.jsm"); michael@0: XPCOMUtils.defineLazyServiceGetter(this, "gELS", michael@0: "@mozilla.org/eventlistenerservice;1", "nsIEventListenerService"); michael@0: michael@0: const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; michael@0: michael@0: const kSpecialWidgetPfx = "customizableui-special-"; michael@0: michael@0: const kPrefCustomizationState = "browser.uiCustomization.state"; michael@0: const kPrefCustomizationAutoAdd = "browser.uiCustomization.autoAdd"; michael@0: const kPrefCustomizationDebug = "browser.uiCustomization.debug"; michael@0: const kPrefDrawInTitlebar = "browser.tabs.drawInTitlebar"; michael@0: michael@0: /** michael@0: * The keys are the handlers that are fired when the event type (the value) michael@0: * is fired on the subview. A widget that provides a subview has the option michael@0: * of providing onViewShowing and onViewHiding event handlers. michael@0: */ michael@0: const kSubviewEvents = [ michael@0: "ViewShowing", michael@0: "ViewHiding" michael@0: ]; michael@0: michael@0: /** michael@0: * gPalette is a map of every widget that CustomizableUI.jsm knows about, keyed michael@0: * on their IDs. michael@0: */ michael@0: let gPalette = new Map(); michael@0: michael@0: /** michael@0: * gAreas maps area IDs to Sets of properties about those areas. An area is a michael@0: * place where a widget can be put. michael@0: */ michael@0: let gAreas = new Map(); michael@0: michael@0: /** michael@0: * gPlacements maps area IDs to Arrays of widget IDs, indicating that the widgets michael@0: * are placed within that area (either directly in the area node, or in the michael@0: * customizationTarget of the node). michael@0: */ michael@0: let gPlacements = new Map(); michael@0: michael@0: /** michael@0: * gFuturePlacements represent placements that will happen for areas that have michael@0: * not yet loaded (due to lazy-loading). This can occur when add-ons register michael@0: * widgets. michael@0: */ michael@0: let gFuturePlacements = new Map(); michael@0: michael@0: //XXXunf Temporary. Need a nice way to abstract functions to build widgets michael@0: // of these types. michael@0: let gSupportedWidgetTypes = new Set(["button", "view", "custom"]); michael@0: michael@0: /** michael@0: * gPanelsForWindow is a list of known panels in a window which we may need to close michael@0: * should command events fire which target them. michael@0: */ michael@0: let gPanelsForWindow = new WeakMap(); michael@0: michael@0: /** michael@0: * gSeenWidgets remembers which widgets the user has seen for the first time michael@0: * before. This way, if a new widget is created, and the user has not seen it michael@0: * before, it can be put in its default location. Otherwise, it remains in the michael@0: * palette. michael@0: */ michael@0: let gSeenWidgets = new Set(); michael@0: michael@0: /** michael@0: * gDirtyAreaCache is a set of area IDs for areas where items have been added, michael@0: * moved or removed at least once. This set is persisted, and is used to michael@0: * optimize building of toolbars in the default case where no toolbars should michael@0: * be "dirty". michael@0: */ michael@0: let gDirtyAreaCache = new Set(); michael@0: michael@0: /** michael@0: * gPendingBuildAreas is a map from area IDs to map from build nodes to their michael@0: * existing children at the time of node registration, that are waiting michael@0: * for the area to be registered michael@0: */ michael@0: let gPendingBuildAreas = new Map(); michael@0: michael@0: let gSavedState = null; michael@0: let gRestoring = false; michael@0: let gDirty = false; michael@0: let gInBatchStack = 0; michael@0: let gResetting = false; michael@0: let gUndoResetting = false; michael@0: michael@0: /** michael@0: * gBuildAreas maps area IDs to actual area nodes within browser windows. michael@0: */ michael@0: let gBuildAreas = new Map(); michael@0: michael@0: /** michael@0: * gBuildWindows is a map of windows that have registered build areas, mapped michael@0: * to a Set of known toolboxes in that window. michael@0: */ michael@0: let gBuildWindows = new Map(); michael@0: michael@0: let gNewElementCount = 0; michael@0: let gGroupWrapperCache = new Map(); michael@0: let gSingleWrapperCache = new WeakMap(); michael@0: let gListeners = new Set(); michael@0: michael@0: let gUIStateBeforeReset = { michael@0: uiCustomizationState: null, michael@0: drawInTitlebar: null, michael@0: }; michael@0: michael@0: let gModuleName = "[CustomizableUI]"; michael@0: #include logging.js michael@0: michael@0: let CustomizableUIInternal = { michael@0: initialize: function() { michael@0: LOG("Initializing"); michael@0: michael@0: this.addListener(this); michael@0: this._defineBuiltInWidgets(); michael@0: this.loadSavedState(); michael@0: michael@0: let panelPlacements = [ michael@0: "edit-controls", michael@0: "zoom-controls", michael@0: "new-window-button", michael@0: "privatebrowsing-button", michael@0: "save-page-button", michael@0: "print-button", michael@0: "history-panelmenu", michael@0: "fullscreen-button", michael@0: "find-button", michael@0: "preferences-button", michael@0: "add-ons-button", michael@0: "developer-button", michael@0: ]; michael@0: michael@0: if (gPalette.has("switch-to-metro-button")) { michael@0: panelPlacements.push("switch-to-metro-button"); michael@0: } michael@0: michael@0: #ifdef NIGHTLY_BUILD michael@0: if (gPalette.has("e10s-button")) { michael@0: let newWindowIndex = panelPlacements.indexOf("new-window-button"); michael@0: if (newWindowIndex > -1) { michael@0: panelPlacements.splice(newWindowIndex + 1, 0, "e10s-button"); michael@0: } michael@0: } michael@0: #endif michael@0: michael@0: let showCharacterEncoding = Services.prefs.getComplexValue( michael@0: "browser.menu.showCharacterEncoding", michael@0: Ci.nsIPrefLocalizedString michael@0: ).data; michael@0: if (showCharacterEncoding == "true") { michael@0: panelPlacements.push("characterencoding-button"); michael@0: } michael@0: michael@0: this.registerArea(CustomizableUI.AREA_PANEL, { michael@0: anchor: "PanelUI-menu-button", michael@0: type: CustomizableUI.TYPE_MENU_PANEL, michael@0: defaultPlacements: panelPlacements michael@0: }, true); michael@0: PanelWideWidgetTracker.init(); michael@0: michael@0: this.registerArea(CustomizableUI.AREA_NAVBAR, { michael@0: legacy: true, michael@0: type: CustomizableUI.TYPE_TOOLBAR, michael@0: overflowable: true, michael@0: defaultPlacements: [ michael@0: "urlbar-container", michael@0: "search-container", michael@0: "webrtc-status-button", michael@0: "bookmarks-menu-button", michael@0: "downloads-button", michael@0: "home-button", michael@0: "social-share-button", michael@0: ], michael@0: defaultCollapsed: false, michael@0: }, true); michael@0: #ifndef XP_MACOSX michael@0: this.registerArea(CustomizableUI.AREA_MENUBAR, { michael@0: legacy: true, michael@0: type: CustomizableUI.TYPE_TOOLBAR, michael@0: defaultPlacements: [ michael@0: "menubar-items", michael@0: ], michael@0: get defaultCollapsed() { michael@0: #ifdef MENUBAR_CAN_AUTOHIDE michael@0: #if defined(MOZ_WIDGET_GTK) || defined(MOZ_WIDGET_QT) michael@0: return true; michael@0: #else michael@0: // This is duplicated logic from /browser/base/jar.mn michael@0: // for win6BrowserOverlay.xul. michael@0: return Services.appinfo.OS == "WINNT" && michael@0: Services.sysinfo.getProperty("version") != "5.1"; michael@0: #endif michael@0: #endif michael@0: return false; michael@0: } michael@0: }, true); michael@0: #endif michael@0: this.registerArea(CustomizableUI.AREA_TABSTRIP, { michael@0: legacy: true, michael@0: type: CustomizableUI.TYPE_TOOLBAR, michael@0: defaultPlacements: [ michael@0: "tabbrowser-tabs", michael@0: "new-tab-button", michael@0: "alltabs-button", michael@0: ], michael@0: defaultCollapsed: null, michael@0: }, true); michael@0: this.registerArea(CustomizableUI.AREA_BOOKMARKS, { michael@0: legacy: true, michael@0: type: CustomizableUI.TYPE_TOOLBAR, michael@0: defaultPlacements: [ michael@0: "personal-bookmarks", michael@0: ], michael@0: defaultCollapsed: true, michael@0: }, true); michael@0: michael@0: this.registerArea(CustomizableUI.AREA_ADDONBAR, { michael@0: type: CustomizableUI.TYPE_TOOLBAR, michael@0: legacy: true, michael@0: defaultPlacements: ["addonbar-closebutton", "status-bar"], michael@0: defaultCollapsed: false, michael@0: }, true); michael@0: }, michael@0: michael@0: get _builtinToolbars() { michael@0: return new Set([ michael@0: CustomizableUI.AREA_NAVBAR, michael@0: CustomizableUI.AREA_BOOKMARKS, michael@0: CustomizableUI.AREA_TABSTRIP, michael@0: CustomizableUI.AREA_ADDONBAR, michael@0: #ifndef XP_MACOSX michael@0: CustomizableUI.AREA_MENUBAR, michael@0: #endif michael@0: ]); michael@0: }, michael@0: michael@0: _defineBuiltInWidgets: function() { michael@0: //XXXunf Need to figure out how to auto-add new builtin widgets in new michael@0: // app versions to already customized areas. michael@0: for (let widgetDefinition of CustomizableWidgets) { michael@0: this.createBuiltinWidget(widgetDefinition); michael@0: } michael@0: }, michael@0: michael@0: wrapWidget: function(aWidgetId) { michael@0: if (gGroupWrapperCache.has(aWidgetId)) { michael@0: return gGroupWrapperCache.get(aWidgetId); michael@0: } michael@0: michael@0: let provider = this.getWidgetProvider(aWidgetId); michael@0: if (!provider) { michael@0: return null; michael@0: } michael@0: michael@0: if (provider == CustomizableUI.PROVIDER_API) { michael@0: let widget = gPalette.get(aWidgetId); michael@0: if (!widget.wrapper) { michael@0: widget.wrapper = new WidgetGroupWrapper(widget); michael@0: gGroupWrapperCache.set(aWidgetId, widget.wrapper); michael@0: } michael@0: return widget.wrapper; michael@0: } michael@0: michael@0: // PROVIDER_SPECIAL gets treated the same as PROVIDER_XUL. michael@0: let wrapper = new XULWidgetGroupWrapper(aWidgetId); michael@0: gGroupWrapperCache.set(aWidgetId, wrapper); michael@0: return wrapper; michael@0: }, michael@0: michael@0: registerArea: function(aName, aProperties, aInternalCaller) { michael@0: if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) { michael@0: throw new Error("Invalid area name"); michael@0: } michael@0: michael@0: let areaIsKnown = gAreas.has(aName); michael@0: let props = areaIsKnown ? gAreas.get(aName) : new Map(); michael@0: const kImmutableProperties = new Set(["type", "legacy", "overflowable"]); michael@0: for (let key in aProperties) { michael@0: if (areaIsKnown && kImmutableProperties.has(key) && michael@0: props.get(key) != aProperties[key]) { michael@0: throw new Error("An area cannot change the property for '" + key + "'"); michael@0: } michael@0: //XXXgijs for special items, we need to make sure they have an appropriate ID michael@0: // so we aren't perpetually in a non-default state: michael@0: if (key == "defaultPlacements" && Array.isArray(aProperties[key])) { michael@0: props.set(key, aProperties[key].map(x => this.isSpecialWidget(x) ? this.ensureSpecialWidgetId(x) : x )); michael@0: } else { michael@0: props.set(key, aProperties[key]); michael@0: } michael@0: } michael@0: // Default to a toolbar: michael@0: if (!props.has("type")) { michael@0: props.set("type", CustomizableUI.TYPE_TOOLBAR); michael@0: } michael@0: if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { michael@0: // Check aProperties instead of props because this check is only interested michael@0: // in the passed arguments, not the state of a potentially pre-existing area. michael@0: if (!aInternalCaller && aProperties["defaultCollapsed"]) { michael@0: throw new Error("defaultCollapsed is only allowed for default toolbars.") michael@0: } michael@0: if (!props.has("defaultCollapsed")) { michael@0: props.set("defaultCollapsed", true); michael@0: } michael@0: } else if (props.has("defaultCollapsed")) { michael@0: throw new Error("defaultCollapsed only applies for TYPE_TOOLBAR areas."); michael@0: } michael@0: // Sanity check type: michael@0: let allTypes = [CustomizableUI.TYPE_TOOLBAR, CustomizableUI.TYPE_MENU_PANEL]; michael@0: if (allTypes.indexOf(props.get("type")) == -1) { michael@0: throw new Error("Invalid area type " + props.get("type")); michael@0: } michael@0: michael@0: // And to no placements: michael@0: if (!props.has("defaultPlacements")) { michael@0: props.set("defaultPlacements", []); michael@0: } michael@0: // Sanity check default placements array: michael@0: if (!Array.isArray(props.get("defaultPlacements"))) { michael@0: throw new Error("Should provide an array of default placements"); michael@0: } michael@0: michael@0: if (!areaIsKnown) { michael@0: gAreas.set(aName, props); michael@0: michael@0: if (props.get("legacy") && !gPlacements.has(aName)) { michael@0: // Guarantee this area exists in gFuturePlacements, to avoid checking it in michael@0: // various places elsewhere. michael@0: gFuturePlacements.set(aName, new Set()); michael@0: } else { michael@0: this.restoreStateForArea(aName); michael@0: } michael@0: michael@0: // If we have pending build area nodes, register all of them michael@0: if (gPendingBuildAreas.has(aName)) { michael@0: let pendingNodes = gPendingBuildAreas.get(aName); michael@0: for (let [pendingNode, existingChildren] of pendingNodes) { michael@0: this.registerToolbarNode(pendingNode, existingChildren); michael@0: } michael@0: gPendingBuildAreas.delete(aName); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: unregisterArea: function(aName, aDestroyPlacements) { michael@0: if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) { michael@0: throw new Error("Invalid area name"); michael@0: } michael@0: if (!gAreas.has(aName) && !gPlacements.has(aName)) { michael@0: throw new Error("Area not registered"); michael@0: } michael@0: michael@0: // Move all the widgets out michael@0: this.beginBatchUpdate(); michael@0: try { michael@0: let placements = gPlacements.get(aName); michael@0: if (placements) { michael@0: // Need to clone this array so removeWidgetFromArea doesn't modify it michael@0: placements = [...placements]; michael@0: placements.forEach(this.removeWidgetFromArea, this); michael@0: } michael@0: michael@0: // Delete all remaining traces. michael@0: gAreas.delete(aName); michael@0: // Only destroy placements when necessary: michael@0: if (aDestroyPlacements) { michael@0: gPlacements.delete(aName); michael@0: } else { michael@0: // Otherwise we need to re-set them, as removeFromArea will have emptied michael@0: // them out: michael@0: gPlacements.set(aName, placements); michael@0: } michael@0: gFuturePlacements.delete(aName); michael@0: let existingAreaNodes = gBuildAreas.get(aName); michael@0: if (existingAreaNodes) { michael@0: for (let areaNode of existingAreaNodes) { michael@0: this.notifyListeners("onAreaNodeUnregistered", aName, areaNode.customizationTarget, michael@0: CustomizableUI.REASON_AREA_UNREGISTERED); michael@0: } michael@0: } michael@0: gBuildAreas.delete(aName); michael@0: } finally { michael@0: this.endBatchUpdate(true); michael@0: } michael@0: }, michael@0: michael@0: registerToolbarNode: function(aToolbar, aExistingChildren) { michael@0: let area = aToolbar.id; michael@0: if (gBuildAreas.has(area) && gBuildAreas.get(area).has(aToolbar)) { michael@0: return; michael@0: } michael@0: let document = aToolbar.ownerDocument; michael@0: let areaProperties = gAreas.get(area); michael@0: michael@0: // If this area is not registered, try to do it automatically: michael@0: if (!areaProperties) { michael@0: // If there's no defaultset attribute and this isn't a legacy extra toolbar, michael@0: // we assume that we should wait for registerArea to be called: michael@0: if (!aToolbar.hasAttribute("defaultset") && michael@0: !aToolbar.hasAttribute("customindex")) { michael@0: if (!gPendingBuildAreas.has(area)) { michael@0: gPendingBuildAreas.set(area, new Map()); michael@0: } michael@0: let pendingNodes = gPendingBuildAreas.get(area); michael@0: pendingNodes.set(aToolbar, aExistingChildren); michael@0: return; michael@0: } michael@0: let props = {type: CustomizableUI.TYPE_TOOLBAR, legacy: true}; michael@0: let defaultsetAttribute = aToolbar.getAttribute("defaultset") || ""; michael@0: props.defaultPlacements = defaultsetAttribute.split(',').filter(s => s); michael@0: this.registerArea(area, props); michael@0: areaProperties = gAreas.get(area); michael@0: } michael@0: michael@0: this.beginBatchUpdate(); michael@0: try { michael@0: let placements = gPlacements.get(area); michael@0: if (!placements && areaProperties.has("legacy")) { michael@0: let legacyState = aToolbar.getAttribute("currentset"); michael@0: if (legacyState) { michael@0: legacyState = legacyState.split(",").filter(s => s); michael@0: } michael@0: michael@0: // Manually restore the state here, so the legacy state can be converted. michael@0: this.restoreStateForArea(area, legacyState); michael@0: placements = gPlacements.get(area); michael@0: } michael@0: michael@0: // Check that the current children and the current placements match. If michael@0: // not, mark it as dirty: michael@0: if (aExistingChildren.length != placements.length || michael@0: aExistingChildren.every((id, i) => id == placements[i])) { michael@0: gDirtyAreaCache.add(area); michael@0: } michael@0: michael@0: if (areaProperties.has("overflowable")) { michael@0: aToolbar.overflowable = new OverflowableToolbar(aToolbar); michael@0: } michael@0: michael@0: this.registerBuildArea(area, aToolbar); michael@0: michael@0: // We only build the toolbar if it's been marked as "dirty". Dirty means michael@0: // one of the following things: michael@0: // 1) Items have been added, moved or removed from this toolbar before. michael@0: // 2) The number of children of the toolbar does not match the length of michael@0: // the placements array for that area. michael@0: // michael@0: // This notion of being "dirty" is stored in a cache which is persisted michael@0: // in the saved state. michael@0: if (gDirtyAreaCache.has(area)) { michael@0: this.buildArea(area, placements, aToolbar); michael@0: } michael@0: this.notifyListeners("onAreaNodeRegistered", area, aToolbar.customizationTarget); michael@0: aToolbar.setAttribute("currentset", placements.join(",")); michael@0: } finally { michael@0: this.endBatchUpdate(); michael@0: } michael@0: }, michael@0: michael@0: buildArea: function(aArea, aPlacements, aAreaNode) { michael@0: let document = aAreaNode.ownerDocument; michael@0: let window = document.defaultView; michael@0: let inPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(window); michael@0: let container = aAreaNode.customizationTarget; michael@0: let areaIsPanel = gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL; michael@0: michael@0: if (!container) { michael@0: throw new Error("Expected area " + aArea michael@0: + " to have a customizationTarget attribute."); michael@0: } michael@0: michael@0: // Restore nav-bar visibility since it may have been hidden michael@0: // through a migration path (bug 938980) or an add-on. michael@0: if (aArea == CustomizableUI.AREA_NAVBAR) { michael@0: aAreaNode.collapsed = false; michael@0: } michael@0: michael@0: this.beginBatchUpdate(); michael@0: michael@0: try { michael@0: let currentNode = container.firstChild; michael@0: let placementsToRemove = new Set(); michael@0: for (let id of aPlacements) { michael@0: while (currentNode && currentNode.getAttribute("skipintoolbarset") == "true") { michael@0: currentNode = currentNode.nextSibling; michael@0: } michael@0: michael@0: if (currentNode && currentNode.id == id) { michael@0: currentNode = currentNode.nextSibling; michael@0: continue; michael@0: } michael@0: michael@0: if (this.isSpecialWidget(id) && areaIsPanel) { michael@0: placementsToRemove.add(id); michael@0: continue; michael@0: } michael@0: michael@0: let [provider, node] = this.getWidgetNode(id, window); michael@0: if (!node) { michael@0: LOG("Unknown widget: " + id); michael@0: continue; michael@0: } michael@0: michael@0: // If the placements have items in them which are (now) no longer removable, michael@0: // we shouldn't be moving them: michael@0: if (provider == CustomizableUI.PROVIDER_API) { michael@0: let widgetInfo = gPalette.get(id); michael@0: if (!widgetInfo.removable && aArea != widgetInfo.defaultArea) { michael@0: placementsToRemove.add(id); michael@0: continue; michael@0: } michael@0: } else if (provider == CustomizableUI.PROVIDER_XUL && michael@0: node.parentNode != container && !this.isWidgetRemovable(node)) { michael@0: placementsToRemove.add(id); michael@0: continue; michael@0: } // Special widgets are always removable, so no need to check them michael@0: michael@0: if (inPrivateWindow && provider == CustomizableUI.PROVIDER_API) { michael@0: let widget = gPalette.get(id); michael@0: if (!widget.showInPrivateBrowsing && inPrivateWindow) { michael@0: continue; michael@0: } michael@0: } michael@0: michael@0: this.ensureButtonContextMenu(node, aAreaNode); michael@0: if (node.localName == "toolbarbutton") { michael@0: if (areaIsPanel) { michael@0: node.setAttribute("wrap", "true"); michael@0: } else { michael@0: node.removeAttribute("wrap"); michael@0: } michael@0: } michael@0: michael@0: this.insertWidgetBefore(node, currentNode, container, aArea); michael@0: if (gResetting) { michael@0: this.notifyListeners("onWidgetReset", node, container); michael@0: } else if (gUndoResetting) { michael@0: this.notifyListeners("onWidgetUndoMove", node, container); michael@0: } michael@0: } michael@0: michael@0: if (currentNode) { michael@0: let palette = aAreaNode.toolbox ? aAreaNode.toolbox.palette : null; michael@0: let limit = currentNode.previousSibling; michael@0: let node = container.lastChild; michael@0: while (node && node != limit) { michael@0: let previousSibling = node.previousSibling; michael@0: // Nodes opt-in to removability. If they're removable, and we haven't michael@0: // seen them in the placements array, then we toss them into the palette michael@0: // if one exists. If no palette exists, we just remove the node. If the michael@0: // node is not removable, we leave it where it is. However, we can only michael@0: // safely touch elements that have an ID - both because we depend on michael@0: // IDs, and because such elements are not intended to be widgets michael@0: // (eg, titlebar-placeholder elements). michael@0: if (node.id && node.getAttribute("skipintoolbarset") != "true") { michael@0: if (this.isWidgetRemovable(node)) { michael@0: if (palette && !this.isSpecialWidget(node.id)) { michael@0: palette.appendChild(node); michael@0: this.removeLocationAttributes(node); michael@0: } else { michael@0: container.removeChild(node); michael@0: } michael@0: } else { michael@0: this.setLocationAttributes(currentNode, aArea); michael@0: node.setAttribute("removable", false); michael@0: LOG("Adding non-removable widget to placements of " + aArea + ": " + michael@0: node.id); michael@0: gPlacements.get(aArea).push(node.id); michael@0: gDirty = true; michael@0: } michael@0: } michael@0: node = previousSibling; michael@0: } michael@0: } michael@0: michael@0: // If there are placements in here which aren't removable from their original area, michael@0: // we remove them from this area's placement array. They will (have) be(en) added michael@0: // to their original area's placements array in the block above this one. michael@0: if (placementsToRemove.size) { michael@0: let placementAry = gPlacements.get(aArea); michael@0: for (let id of placementsToRemove) { michael@0: let index = placementAry.indexOf(id); michael@0: placementAry.splice(index, 1); michael@0: } michael@0: } michael@0: michael@0: if (gResetting) { michael@0: this.notifyListeners("onAreaReset", aArea, container); michael@0: } michael@0: } finally { michael@0: this.endBatchUpdate(); michael@0: } michael@0: }, michael@0: michael@0: addPanelCloseListeners: function(aPanel) { michael@0: gELS.addSystemEventListener(aPanel, "click", this, false); michael@0: gELS.addSystemEventListener(aPanel, "keypress", this, false); michael@0: let win = aPanel.ownerDocument.defaultView; michael@0: if (!gPanelsForWindow.has(win)) { michael@0: gPanelsForWindow.set(win, new Set()); michael@0: } michael@0: gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel)); michael@0: }, michael@0: michael@0: removePanelCloseListeners: function(aPanel) { michael@0: gELS.removeSystemEventListener(aPanel, "click", this, false); michael@0: gELS.removeSystemEventListener(aPanel, "keypress", this, false); michael@0: let win = aPanel.ownerDocument.defaultView; michael@0: let panels = gPanelsForWindow.get(win); michael@0: if (panels) { michael@0: panels.delete(this._getPanelForNode(aPanel)); michael@0: } michael@0: }, michael@0: michael@0: ensureButtonContextMenu: function(aNode, aAreaNode) { michael@0: const kPanelItemContextMenu = "customizationPanelItemContextMenu"; michael@0: michael@0: let currentContextMenu = aNode.getAttribute("context") || michael@0: aNode.getAttribute("contextmenu"); michael@0: let place = CustomizableUI.getPlaceForItem(aAreaNode); michael@0: let contextMenuForPlace = place == "panel" ? michael@0: kPanelItemContextMenu : michael@0: null; michael@0: if (contextMenuForPlace && !currentContextMenu) { michael@0: aNode.setAttribute("context", contextMenuForPlace); michael@0: } else if (currentContextMenu == kPanelItemContextMenu && michael@0: contextMenuForPlace != kPanelItemContextMenu) { michael@0: aNode.removeAttribute("context"); michael@0: aNode.removeAttribute("contextmenu"); michael@0: } michael@0: }, michael@0: michael@0: getWidgetProvider: function(aWidgetId) { michael@0: if (this.isSpecialWidget(aWidgetId)) { michael@0: return CustomizableUI.PROVIDER_SPECIAL; michael@0: } michael@0: if (gPalette.has(aWidgetId)) { michael@0: return CustomizableUI.PROVIDER_API; michael@0: } michael@0: // If this was an API widget that was destroyed, return null: michael@0: if (gSeenWidgets.has(aWidgetId)) { michael@0: return null; michael@0: } michael@0: michael@0: // We fall back to the XUL provider, but we don't know for sure (at this michael@0: // point) whether it exists there either. So the API is technically lying. michael@0: // Ideally, it would be able to return an error value (or throw an michael@0: // exception) if it really didn't exist. Our code calling this function michael@0: // handles that fine, but this is a public API. michael@0: return CustomizableUI.PROVIDER_XUL; michael@0: }, michael@0: michael@0: getWidgetNode: function(aWidgetId, aWindow) { michael@0: let document = aWindow.document; michael@0: michael@0: if (this.isSpecialWidget(aWidgetId)) { michael@0: let widgetNode = document.getElementById(aWidgetId) || michael@0: this.createSpecialWidget(aWidgetId, document); michael@0: return [ CustomizableUI.PROVIDER_SPECIAL, widgetNode]; michael@0: } michael@0: michael@0: let widget = gPalette.get(aWidgetId); michael@0: if (widget) { michael@0: // If we have an instance of this widget already, just use that. michael@0: if (widget.instances.has(document)) { michael@0: LOG("An instance of widget " + aWidgetId + " already exists in this " michael@0: + "document. Reusing."); michael@0: return [ CustomizableUI.PROVIDER_API, michael@0: widget.instances.get(document) ]; michael@0: } michael@0: michael@0: return [ CustomizableUI.PROVIDER_API, michael@0: this.buildWidget(document, widget) ]; michael@0: } michael@0: michael@0: LOG("Searching for " + aWidgetId + " in toolbox."); michael@0: let node = this.findWidgetInWindow(aWidgetId, aWindow); michael@0: if (node) { michael@0: return [ CustomizableUI.PROVIDER_XUL, node ]; michael@0: } michael@0: michael@0: LOG("No node for " + aWidgetId + " found."); michael@0: return [null, null]; michael@0: }, michael@0: michael@0: registerMenuPanel: function(aPanelContents) { michael@0: if (gBuildAreas.has(CustomizableUI.AREA_PANEL) && michael@0: gBuildAreas.get(CustomizableUI.AREA_PANEL).has(aPanelContents)) { michael@0: return; michael@0: } michael@0: michael@0: let document = aPanelContents.ownerDocument; michael@0: michael@0: aPanelContents.toolbox = document.getElementById("navigator-toolbox"); michael@0: aPanelContents.customizationTarget = aPanelContents; michael@0: michael@0: this.addPanelCloseListeners(this._getPanelForNode(aPanelContents)); michael@0: michael@0: let placements = gPlacements.get(CustomizableUI.AREA_PANEL); michael@0: this.buildArea(CustomizableUI.AREA_PANEL, placements, aPanelContents); michael@0: this.notifyListeners("onAreaNodeRegistered", CustomizableUI.AREA_PANEL, aPanelContents); michael@0: michael@0: for (let child of aPanelContents.children) { michael@0: if (child.localName != "toolbarbutton") { michael@0: if (child.localName == "toolbaritem") { michael@0: this.ensureButtonContextMenu(child, aPanelContents); michael@0: } michael@0: continue; michael@0: } michael@0: this.ensureButtonContextMenu(child, aPanelContents); michael@0: child.setAttribute("wrap", "true"); michael@0: } michael@0: michael@0: this.registerBuildArea(CustomizableUI.AREA_PANEL, aPanelContents); michael@0: }, michael@0: michael@0: onWidgetAdded: function(aWidgetId, aArea, aPosition) { michael@0: this.insertNode(aWidgetId, aArea, aPosition, true); michael@0: michael@0: if (!gResetting) { michael@0: this._clearPreviousUIState(); michael@0: } michael@0: }, michael@0: michael@0: onWidgetRemoved: function(aWidgetId, aArea) { michael@0: let areaNodes = gBuildAreas.get(aArea); michael@0: if (!areaNodes) { michael@0: return; michael@0: } michael@0: michael@0: let area = gAreas.get(aArea); michael@0: let isToolbar = area.get("type") == CustomizableUI.TYPE_TOOLBAR; michael@0: let isOverflowable = isToolbar && area.get("overflowable"); michael@0: let showInPrivateBrowsing = gPalette.has(aWidgetId) michael@0: ? gPalette.get(aWidgetId).showInPrivateBrowsing michael@0: : true; michael@0: michael@0: for (let areaNode of areaNodes) { michael@0: let window = areaNode.ownerDocument.defaultView; michael@0: if (!showInPrivateBrowsing && michael@0: PrivateBrowsingUtils.isWindowPrivate(window)) { michael@0: continue; michael@0: } michael@0: michael@0: let widgetNode = window.document.getElementById(aWidgetId); michael@0: if (!widgetNode) { michael@0: INFO("Widget not found, unable to remove"); michael@0: continue; michael@0: } michael@0: let container = areaNode.customizationTarget; michael@0: if (isOverflowable) { michael@0: container = areaNode.overflowable.getContainerFor(widgetNode); michael@0: } michael@0: michael@0: this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null, container, true); michael@0: michael@0: // We remove location attributes here to make sure they're gone too when a michael@0: // widget is removed from a toolbar to the palette. See bug 930950. michael@0: this.removeLocationAttributes(widgetNode); michael@0: // We also need to remove the panel context menu if it's there: michael@0: this.ensureButtonContextMenu(widgetNode); michael@0: widgetNode.removeAttribute("wrap"); michael@0: if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) { michael@0: container.removeChild(widgetNode); michael@0: } else { michael@0: areaNode.toolbox.palette.appendChild(widgetNode); michael@0: } michael@0: this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null, container, true); michael@0: michael@0: if (isToolbar) { michael@0: areaNode.setAttribute("currentset", gPlacements.get(aArea).join(',')); michael@0: } michael@0: michael@0: let windowCache = gSingleWrapperCache.get(window); michael@0: if (windowCache) { michael@0: windowCache.delete(aWidgetId); michael@0: } michael@0: } michael@0: if (!gResetting) { michael@0: this._clearPreviousUIState(); michael@0: } michael@0: }, michael@0: michael@0: onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) { michael@0: this.insertNode(aWidgetId, aArea, aNewPosition); michael@0: if (!gResetting) { michael@0: this._clearPreviousUIState(); michael@0: } michael@0: }, michael@0: michael@0: onCustomizeEnd: function(aWindow) { michael@0: this._clearPreviousUIState(); michael@0: }, michael@0: michael@0: registerBuildArea: function(aArea, aNode) { michael@0: // We ensure that the window is registered to have its customization data michael@0: // cleaned up when unloading. michael@0: let window = aNode.ownerDocument.defaultView; michael@0: if (window.closed) { michael@0: return; michael@0: } michael@0: this.registerBuildWindow(window); michael@0: michael@0: // Also register this build area's toolbox. michael@0: if (aNode.toolbox) { michael@0: gBuildWindows.get(window).add(aNode.toolbox); michael@0: } michael@0: michael@0: if (!gBuildAreas.has(aArea)) { michael@0: gBuildAreas.set(aArea, new Set()); michael@0: } michael@0: michael@0: gBuildAreas.get(aArea).add(aNode); michael@0: michael@0: // Give a class to all customize targets to be used for styling in Customize Mode michael@0: let customizableNode = this.getCustomizeTargetForArea(aArea, window); michael@0: customizableNode.classList.add("customization-target"); michael@0: }, michael@0: michael@0: registerBuildWindow: function(aWindow) { michael@0: if (!gBuildWindows.has(aWindow)) { michael@0: gBuildWindows.set(aWindow, new Set()); michael@0: michael@0: aWindow.addEventListener("unload", this); michael@0: aWindow.addEventListener("command", this, true); michael@0: michael@0: this.notifyListeners("onWindowOpened", aWindow); michael@0: } michael@0: }, michael@0: michael@0: unregisterBuildWindow: function(aWindow) { michael@0: aWindow.removeEventListener("unload", this); michael@0: aWindow.removeEventListener("command", this, true); michael@0: gPanelsForWindow.delete(aWindow); michael@0: gBuildWindows.delete(aWindow); michael@0: gSingleWrapperCache.delete(aWindow); michael@0: let document = aWindow.document; michael@0: michael@0: for (let [areaId, areaNodes] of gBuildAreas) { michael@0: let areaProperties = gAreas.get(areaId); michael@0: for (let node of areaNodes) { michael@0: if (node.ownerDocument == document) { michael@0: this.notifyListeners("onAreaNodeUnregistered", areaId, node.customizationTarget, michael@0: CustomizableUI.REASON_WINDOW_CLOSED); michael@0: if (areaProperties.has("overflowable")) { michael@0: node.overflowable.uninit(); michael@0: node.overflowable = null; michael@0: } michael@0: areaNodes.delete(node); michael@0: } michael@0: } michael@0: } michael@0: michael@0: for (let [,widget] of gPalette) { michael@0: widget.instances.delete(document); michael@0: this.notifyListeners("onWidgetInstanceRemoved", widget.id, document); michael@0: } michael@0: michael@0: for (let [area, areaMap] of gPendingBuildAreas) { michael@0: let toDelete = []; michael@0: for (let [areaNode, ] of areaMap) { michael@0: if (areaNode.ownerDocument == document) { michael@0: toDelete.push(areaNode); michael@0: } michael@0: } michael@0: for (let areaNode of toDelete) { michael@0: areaMap.delete(toDelete); michael@0: } michael@0: } michael@0: michael@0: this.notifyListeners("onWindowClosed", aWindow); michael@0: }, michael@0: michael@0: setLocationAttributes: function(aNode, aArea) { michael@0: let props = gAreas.get(aArea); michael@0: if (!props) { michael@0: throw new Error("Expected area " + aArea + " to have a properties Map " + michael@0: "associated with it."); michael@0: } michael@0: michael@0: aNode.setAttribute("cui-areatype", props.get("type") || ""); michael@0: let anchor = props.get("anchor"); michael@0: if (anchor) { michael@0: aNode.setAttribute("cui-anchorid", anchor); michael@0: } else { michael@0: aNode.removeAttribute("cui-anchorid"); michael@0: } michael@0: }, michael@0: michael@0: removeLocationAttributes: function(aNode) { michael@0: aNode.removeAttribute("cui-areatype"); michael@0: aNode.removeAttribute("cui-anchorid"); michael@0: }, michael@0: michael@0: insertNode: function(aWidgetId, aArea, aPosition, isNew) { michael@0: let areaNodes = gBuildAreas.get(aArea); michael@0: if (!areaNodes) { michael@0: return; michael@0: } michael@0: michael@0: let placements = gPlacements.get(aArea); michael@0: if (!placements) { michael@0: ERROR("Could not find any placements for " + aArea + michael@0: " when moving a widget."); michael@0: return; michael@0: } michael@0: michael@0: // Go through each of the nodes associated with this area and move the michael@0: // widget to the requested location. michael@0: for (let areaNode of areaNodes) { michael@0: this.insertNodeInWindow(aWidgetId, areaNode, isNew); michael@0: } michael@0: }, michael@0: michael@0: insertNodeInWindow: function(aWidgetId, aAreaNode, isNew) { michael@0: let window = aAreaNode.ownerDocument.defaultView; michael@0: let showInPrivateBrowsing = gPalette.has(aWidgetId) michael@0: ? gPalette.get(aWidgetId).showInPrivateBrowsing michael@0: : true; michael@0: michael@0: if (!showInPrivateBrowsing && PrivateBrowsingUtils.isWindowPrivate(window)) { michael@0: return; michael@0: } michael@0: michael@0: let [, widgetNode] = this.getWidgetNode(aWidgetId, window); michael@0: if (!widgetNode) { michael@0: ERROR("Widget '" + aWidgetId + "' not found, unable to move"); michael@0: return; michael@0: } michael@0: michael@0: let areaId = aAreaNode.id; michael@0: if (isNew) { michael@0: this.ensureButtonContextMenu(widgetNode, aAreaNode); michael@0: if (widgetNode.localName == "toolbarbutton" && areaId == CustomizableUI.AREA_PANEL) { michael@0: widgetNode.setAttribute("wrap", "true"); michael@0: } michael@0: } michael@0: michael@0: let [insertionContainer, nextNode] = this.findInsertionPoints(widgetNode, aAreaNode); michael@0: this.insertWidgetBefore(widgetNode, nextNode, insertionContainer, areaId); michael@0: michael@0: if (gAreas.get(areaId).get("type") == CustomizableUI.TYPE_TOOLBAR) { michael@0: aAreaNode.setAttribute("currentset", gPlacements.get(areaId).join(',')); michael@0: } michael@0: }, michael@0: michael@0: findInsertionPoints: function(aNode, aAreaNode) { michael@0: let areaId = aAreaNode.id; michael@0: let props = gAreas.get(areaId); michael@0: michael@0: // For overflowable toolbars, rely on them (because the work is more complicated): michael@0: if (props.get("type") == CustomizableUI.TYPE_TOOLBAR && props.get("overflowable")) { michael@0: return aAreaNode.overflowable.findOverflowedInsertionPoints(aNode); michael@0: } michael@0: michael@0: let container = aAreaNode.customizationTarget; michael@0: let placements = gPlacements.get(areaId); michael@0: let nodeIndex = placements.indexOf(aNode.id); michael@0: michael@0: while (++nodeIndex < placements.length) { michael@0: let nextNodeId = placements[nodeIndex]; michael@0: let nextNode = container.getElementsByAttribute("id", nextNodeId).item(0); michael@0: michael@0: if (nextNode) { michael@0: return [container, nextNode]; michael@0: } michael@0: } michael@0: michael@0: return [container, null]; michael@0: }, michael@0: michael@0: insertWidgetBefore: function(aNode, aNextNode, aContainer, aArea) { michael@0: this.notifyListeners("onWidgetBeforeDOMChange", aNode, aNextNode, aContainer); michael@0: this.setLocationAttributes(aNode, aArea); michael@0: aContainer.insertBefore(aNode, aNextNode); michael@0: this.notifyListeners("onWidgetAfterDOMChange", aNode, aNextNode, aContainer); michael@0: }, michael@0: michael@0: handleEvent: function(aEvent) { michael@0: switch (aEvent.type) { michael@0: case "command": michael@0: if (!this._originalEventInPanel(aEvent)) { michael@0: break; michael@0: } michael@0: aEvent = aEvent.sourceEvent; michael@0: // Fall through michael@0: case "click": michael@0: case "keypress": michael@0: this.maybeAutoHidePanel(aEvent); michael@0: break; michael@0: case "unload": michael@0: this.unregisterBuildWindow(aEvent.currentTarget); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: _originalEventInPanel: function(aEvent) { michael@0: let e = aEvent.sourceEvent; michael@0: if (!e) { michael@0: return false; michael@0: } michael@0: let node = this._getPanelForNode(e.target); michael@0: if (!node) { michael@0: return false; michael@0: } michael@0: let win = e.view; michael@0: let panels = gPanelsForWindow.get(win); michael@0: return !!panels && panels.has(node); michael@0: }, michael@0: michael@0: isSpecialWidget: function(aId) { michael@0: return (aId.startsWith(kSpecialWidgetPfx) || michael@0: aId.startsWith("separator") || michael@0: aId.startsWith("spring") || michael@0: aId.startsWith("spacer")); michael@0: }, michael@0: michael@0: ensureSpecialWidgetId: function(aId) { michael@0: let nodeType = aId.match(/spring|spacer|separator/)[0]; michael@0: // If the ID we were passed isn't a generated one, generate one now: michael@0: if (nodeType == aId) { michael@0: // Ids are differentiated through a unique count suffix. michael@0: return kSpecialWidgetPfx + aId + (++gNewElementCount); michael@0: } michael@0: return aId; michael@0: }, michael@0: michael@0: createSpecialWidget: function(aId, aDocument) { michael@0: let nodeName = "toolbar" + aId.match(/spring|spacer|separator/)[0]; michael@0: let node = aDocument.createElementNS(kNSXUL, nodeName); michael@0: node.id = this.ensureSpecialWidgetId(aId); michael@0: if (nodeName == "toolbarspring") { michael@0: node.flex = 1; michael@0: } michael@0: return node; michael@0: }, michael@0: michael@0: /* Find a XUL-provided widget in a window. Don't try to use this michael@0: * for an API-provided widget or a special widget. michael@0: */ michael@0: findWidgetInWindow: function(aId, aWindow) { michael@0: if (!gBuildWindows.has(aWindow)) { michael@0: throw new Error("Build window not registered"); michael@0: } michael@0: michael@0: if (!aId) { michael@0: ERROR("findWidgetInWindow was passed an empty string."); michael@0: return null; michael@0: } michael@0: michael@0: let document = aWindow.document; michael@0: michael@0: // look for a node with the same id, as the node may be michael@0: // in a different toolbar. michael@0: let node = document.getElementById(aId); michael@0: if (node) { michael@0: let parent = node.parentNode; michael@0: while (parent && !(parent.customizationTarget || michael@0: parent == aWindow.gNavToolbox.palette)) { michael@0: parent = parent.parentNode; michael@0: } michael@0: michael@0: if (parent) { michael@0: let nodeInArea = node.parentNode.localName == "toolbarpaletteitem" ? michael@0: node.parentNode : node; michael@0: // Check if we're in a customization target, or in the palette: michael@0: if ((parent.customizationTarget == nodeInArea.parentNode && michael@0: gBuildWindows.get(aWindow).has(parent.toolbox)) || michael@0: aWindow.gNavToolbox.palette == nodeInArea.parentNode) { michael@0: // Normalize the removable attribute. For backwards compat, if michael@0: // the widget is not located in a toolbox palette then absence michael@0: // of the "removable" attribute means it is not removable. michael@0: if (!node.hasAttribute("removable")) { michael@0: // If we first see this in customization mode, it may be in the michael@0: // customization palette instead of the toolbox palette. michael@0: node.setAttribute("removable", !parent.customizationTarget); michael@0: } michael@0: return node; michael@0: } michael@0: } michael@0: } michael@0: michael@0: let toolboxes = gBuildWindows.get(aWindow); michael@0: for (let toolbox of toolboxes) { michael@0: if (toolbox.palette) { michael@0: // Attempt to locate a node with a matching ID within michael@0: // the palette. michael@0: let node = toolbox.palette.getElementsByAttribute("id", aId)[0]; michael@0: if (node) { michael@0: // Normalize the removable attribute. For backwards compat, this michael@0: // is optional if the widget is located in the toolbox palette, michael@0: // and defaults to *true*, unlike if it was located elsewhere. michael@0: if (!node.hasAttribute("removable")) { michael@0: node.setAttribute("removable", true); michael@0: } michael@0: return node; michael@0: } michael@0: } michael@0: } michael@0: return null; michael@0: }, michael@0: michael@0: buildWidget: function(aDocument, aWidget) { michael@0: if (typeof aWidget == "string") { michael@0: aWidget = gPalette.get(aWidget); michael@0: } michael@0: if (!aWidget) { michael@0: throw new Error("buildWidget was passed a non-widget to build."); michael@0: } michael@0: michael@0: LOG("Building " + aWidget.id + " of type " + aWidget.type); michael@0: michael@0: let node; michael@0: if (aWidget.type == "custom") { michael@0: if (aWidget.onBuild) { michael@0: node = aWidget.onBuild(aDocument); michael@0: } michael@0: if (!node || !(node instanceof aDocument.defaultView.XULElement)) michael@0: ERROR("Custom widget with id " + aWidget.id + " does not return a valid node"); michael@0: } michael@0: else { michael@0: if (aWidget.onBeforeCreated) { michael@0: aWidget.onBeforeCreated(aDocument); michael@0: } michael@0: node = aDocument.createElementNS(kNSXUL, "toolbarbutton"); michael@0: michael@0: node.setAttribute("id", aWidget.id); michael@0: node.setAttribute("widget-id", aWidget.id); michael@0: node.setAttribute("widget-type", aWidget.type); michael@0: if (aWidget.disabled) { michael@0: node.setAttribute("disabled", true); michael@0: } michael@0: node.setAttribute("removable", aWidget.removable); michael@0: node.setAttribute("overflows", aWidget.overflows); michael@0: node.setAttribute("label", this.getLocalizedProperty(aWidget, "label")); michael@0: let additionalTooltipArguments = []; michael@0: if (aWidget.shortcutId) { michael@0: let keyEl = aDocument.getElementById(aWidget.shortcutId); michael@0: if (keyEl) { michael@0: additionalTooltipArguments.push(ShortcutUtils.prettifyShortcut(keyEl)); michael@0: } else { michael@0: ERROR("Key element with id '" + aWidget.shortcutId + "' for widget '" + aWidget.id + michael@0: "' not found!"); michael@0: } michael@0: } michael@0: michael@0: let tooltip = this.getLocalizedProperty(aWidget, "tooltiptext", additionalTooltipArguments); michael@0: node.setAttribute("tooltiptext", tooltip); michael@0: node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional"); michael@0: michael@0: let commandHandler = this.handleWidgetCommand.bind(this, aWidget, node); michael@0: node.addEventListener("command", commandHandler, false); michael@0: let clickHandler = this.handleWidgetClick.bind(this, aWidget, node); michael@0: node.addEventListener("click", clickHandler, false); michael@0: michael@0: // If the widget has a view, and has view showing / hiding listeners, michael@0: // hook those up to this widget. michael@0: if (aWidget.type == "view") { michael@0: LOG("Widget " + aWidget.id + " has a view. Auto-registering event handlers."); michael@0: let viewNode = aDocument.getElementById(aWidget.viewId); michael@0: michael@0: if (viewNode) { michael@0: // PanelUI relies on the .PanelUI-subView class to be able to show only michael@0: // one sub-view at a time. michael@0: viewNode.classList.add("PanelUI-subView"); michael@0: michael@0: for (let eventName of kSubviewEvents) { michael@0: let handler = "on" + eventName; michael@0: if (typeof aWidget[handler] == "function") { michael@0: viewNode.addEventListener(eventName, aWidget[handler], false); michael@0: } michael@0: } michael@0: michael@0: LOG("Widget " + aWidget.id + " showing and hiding event handlers set."); michael@0: } else { michael@0: ERROR("Could not find the view node with id: " + aWidget.viewId + michael@0: ", for widget: " + aWidget.id + "."); michael@0: } michael@0: } michael@0: michael@0: if (aWidget.onCreated) { michael@0: aWidget.onCreated(node); michael@0: } michael@0: } michael@0: michael@0: aWidget.instances.set(aDocument, node); michael@0: return node; michael@0: }, michael@0: michael@0: getLocalizedProperty: function(aWidget, aProp, aFormatArgs, aDef) { michael@0: if (typeof aWidget == "string") { michael@0: aWidget = gPalette.get(aWidget); michael@0: } michael@0: if (!aWidget) { michael@0: throw new Error("getLocalizedProperty was passed a non-widget to work with."); michael@0: } michael@0: let def, name; michael@0: // Let widgets pass their own string identifiers or strings, so that michael@0: // we can use strings which aren't the default (in case string ids change) michael@0: // and so that non-builtin-widgets can also provide labels, tooltips, etc. michael@0: if (aWidget[aProp]) { michael@0: name = aWidget[aProp]; michael@0: // By using this as the default, if a widget provides a full string rather michael@0: // than a string ID for localization, we will fall back to that string michael@0: // and return that. michael@0: def = aDef || name; michael@0: } else { michael@0: name = aWidget.id + "." + aProp; michael@0: def = aDef || ""; michael@0: } michael@0: try { michael@0: if (Array.isArray(aFormatArgs) && aFormatArgs.length) { michael@0: return gWidgetsBundle.formatStringFromName(name, aFormatArgs, michael@0: aFormatArgs.length) || def; michael@0: } michael@0: return gWidgetsBundle.GetStringFromName(name) || def; michael@0: } catch(ex) { michael@0: if (!def) { michael@0: ERROR("Could not localize property '" + name + "'."); michael@0: } michael@0: } michael@0: return def; michael@0: }, michael@0: michael@0: handleWidgetCommand: function(aWidget, aNode, aEvent) { michael@0: LOG("handleWidgetCommand"); michael@0: michael@0: if (aWidget.type == "button") { michael@0: if (aWidget.onCommand) { michael@0: try { michael@0: aWidget.onCommand.call(null, aEvent); michael@0: } catch (e) { michael@0: ERROR(e); michael@0: } michael@0: } else { michael@0: //XXXunf Need to think this through more, and formalize. michael@0: Services.obs.notifyObservers(aNode, michael@0: "customizedui-widget-command", michael@0: aWidget.id); michael@0: } michael@0: } else if (aWidget.type == "view") { michael@0: let ownerWindow = aNode.ownerDocument.defaultView; michael@0: let area = this.getPlacementOfWidget(aNode.id).area; michael@0: let anchor = aNode; michael@0: if (area != CustomizableUI.AREA_PANEL) { michael@0: let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow); michael@0: if (wrapper && wrapper.anchor) { michael@0: this.hidePanelForNode(aNode); michael@0: anchor = wrapper.anchor; michael@0: } michael@0: } michael@0: ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, area); michael@0: } michael@0: }, michael@0: michael@0: handleWidgetClick: function(aWidget, aNode, aEvent) { michael@0: LOG("handleWidgetClick"); michael@0: if (aWidget.onClick) { michael@0: try { michael@0: aWidget.onClick.call(null, aEvent); michael@0: } catch(e) { michael@0: Cu.reportError(e); michael@0: } michael@0: } else { michael@0: //XXXunf Need to think this through more, and formalize. michael@0: Services.obs.notifyObservers(aNode, "customizedui-widget-click", aWidget.id); michael@0: } michael@0: }, michael@0: michael@0: _getPanelForNode: function(aNode) { michael@0: let panel = aNode; michael@0: while (panel && panel.localName != "panel") michael@0: panel = panel.parentNode; michael@0: return panel; michael@0: }, michael@0: michael@0: /* michael@0: * If people put things in the panel which need more than single-click interaction, michael@0: * we don't want to close it. Right now we check for text inputs and menu buttons. michael@0: * We also check for being outside of any toolbaritem/toolbarbutton, ie on a blank michael@0: * part of the menu. michael@0: */ michael@0: _isOnInteractiveElement: function(aEvent) { michael@0: function getMenuPopupForDescendant(aNode) { michael@0: let lastPopup = null; michael@0: while (aNode && aNode.parentNode && michael@0: aNode.parentNode.localName.startsWith("menu")) { michael@0: lastPopup = aNode.localName == "menupopup" ? aNode : lastPopup; michael@0: aNode = aNode.parentNode; michael@0: } michael@0: return lastPopup; michael@0: } michael@0: michael@0: let target = aEvent.originalTarget; michael@0: let panel = this._getPanelForNode(aEvent.currentTarget); michael@0: // This can happen in e.g. customize mode. If there's no panel, michael@0: // there's clearly nothing for us to close; pretend we're interactive. michael@0: if (!panel) { michael@0: return true; michael@0: } michael@0: // We keep track of: michael@0: // whether we're in an input container (text field) michael@0: let inInput = false; michael@0: // whether we're in a popup/context menu michael@0: let inMenu = false; michael@0: // whether we're in a toolbarbutton/toolbaritem michael@0: let inItem = false; michael@0: // whether the current menuitem has a valid closemenu attribute michael@0: let menuitemCloseMenu = "auto"; michael@0: // whether the toolbarbutton/item has a valid closemenu attribute. michael@0: let closemenu = "auto"; michael@0: michael@0: // While keeping track of that, we go from the original target back up, michael@0: // to the panel if we have to. We bail as soon as we find an input, michael@0: // a toolbarbutton/item, or the panel: michael@0: while (true && target) { michael@0: let tagName = target.localName; michael@0: inInput = tagName == "input" || tagName == "textbox"; michael@0: inItem = tagName == "toolbaritem" || tagName == "toolbarbutton"; michael@0: let isMenuItem = tagName == "menuitem"; michael@0: inMenu = inMenu || isMenuItem; michael@0: if (inItem && target.hasAttribute("closemenu")) { michael@0: let closemenuVal = target.getAttribute("closemenu"); michael@0: closemenu = (closemenuVal == "single" || closemenuVal == "none") ? michael@0: closemenuVal : "auto"; michael@0: } michael@0: michael@0: if (isMenuItem && target.hasAttribute("closemenu")) { michael@0: let closemenuVal = target.getAttribute("closemenu"); michael@0: menuitemCloseMenu = (closemenuVal == "single" || closemenuVal == "none") ? michael@0: closemenuVal : "auto"; michael@0: } michael@0: // This isn't in the loop condition because we want to break before michael@0: // changing |target| if any of these conditions are true michael@0: if (inInput || inItem || target == panel) { michael@0: break; michael@0: } michael@0: // We need specific code for popups: the item on which they were invoked michael@0: // isn't necessarily in their parentNode chain: michael@0: if (isMenuItem) { michael@0: let topmostMenuPopup = getMenuPopupForDescendant(target); michael@0: target = (topmostMenuPopup && topmostMenuPopup.triggerNode) || michael@0: target.parentNode; michael@0: } else { michael@0: target = target.parentNode; michael@0: } michael@0: } michael@0: // If the user clicked a menu item... michael@0: if (inMenu) { michael@0: // We care if we're in an input also, michael@0: // or if the user specified closemenu!="auto": michael@0: if (inInput || menuitemCloseMenu != "auto") { michael@0: return true; michael@0: } michael@0: // Otherwise, we're probably fine to close the panel michael@0: return false; michael@0: } michael@0: // If we're not in a menu, and we *are* in a type="menu" toolbarbutton, michael@0: // we'll now interact with the menu michael@0: if (inItem && target.getAttribute("type") == "menu") { michael@0: return true; michael@0: } michael@0: // If we're not in a menu, and we *are* in a type="menu-button" toolbarbutton, michael@0: // it depends whether we're in the dropmarker or the 'real' button: michael@0: if (inItem && target.getAttribute("type") == "menu-button") { michael@0: // 'real' button (which has a single action): michael@0: if (target.getAttribute("anonid") == "button") { michael@0: return closemenu != "none"; michael@0: } michael@0: // otherwise, this is the outer button, and the user will now michael@0: // interact with the menu: michael@0: return true; michael@0: } michael@0: return inInput || !inItem; michael@0: }, michael@0: michael@0: hidePanelForNode: function(aNode) { michael@0: let panel = this._getPanelForNode(aNode); michael@0: if (panel) { michael@0: panel.hidePopup(); michael@0: } michael@0: }, michael@0: michael@0: maybeAutoHidePanel: function(aEvent) { michael@0: if (aEvent.type == "keypress") { michael@0: if (aEvent.keyCode != aEvent.DOM_VK_RETURN) { michael@0: return; michael@0: } michael@0: // If the user hit enter/return, we don't check preventDefault - it makes sense michael@0: // that this was prevented, but we probably still want to close the panel. michael@0: // If consumers don't want this to happen, they should specify the closemenu michael@0: // attribute. michael@0: michael@0: } else if (aEvent.type != "command") { // mouse events: michael@0: if (aEvent.defaultPrevented || aEvent.button != 0) { michael@0: return; michael@0: } michael@0: let isInteractive = this._isOnInteractiveElement(aEvent); michael@0: LOG("maybeAutoHidePanel: interactive ? " + isInteractive); michael@0: if (isInteractive) { michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // We can't use event.target because we might have passed a panelview michael@0: // anonymous content boundary as well, and so target points to the michael@0: // panelmultiview in that case. Unfortunately, this means we get michael@0: // anonymous child nodes instead of the real ones, so looking for the michael@0: // 'stoooop, don't close me' attributes is more involved. michael@0: let target = aEvent.originalTarget; michael@0: let closemenu = "auto"; michael@0: let widgetType = "button"; michael@0: while (target.parentNode && target.localName != "panel") { michael@0: closemenu = target.getAttribute("closemenu"); michael@0: widgetType = target.getAttribute("widget-type"); michael@0: if (closemenu == "none" || closemenu == "single" || michael@0: widgetType == "view") { michael@0: break; michael@0: } michael@0: target = target.parentNode; michael@0: } michael@0: if (closemenu == "none" || widgetType == "view") { michael@0: return; michael@0: } michael@0: michael@0: if (closemenu == "single") { michael@0: let panel = this._getPanelForNode(target); michael@0: let multiview = panel.querySelector("panelmultiview"); michael@0: if (multiview.showingSubView) { michael@0: multiview.showMainView(); michael@0: return; michael@0: } michael@0: } michael@0: michael@0: // If we get here, we can actually hide the popup: michael@0: this.hidePanelForNode(aEvent.target); michael@0: }, michael@0: michael@0: getUnusedWidgets: function(aWindowPalette) { michael@0: let window = aWindowPalette.ownerDocument.defaultView; michael@0: let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window); michael@0: // We use a Set because there can be overlap between the widgets in michael@0: // gPalette and the items in the palette, especially after the first michael@0: // customization, since programmatically generated widgets will remain michael@0: // in the toolbox palette. michael@0: let widgets = new Set(); michael@0: michael@0: // It's possible that some widgets have been defined programmatically and michael@0: // have not been overlayed into the palette. We can find those inside michael@0: // gPalette. michael@0: for (let [id, widget] of gPalette) { michael@0: if (!widget.currentArea) { michael@0: if (widget.showInPrivateBrowsing || !isWindowPrivate) { michael@0: widgets.add(id); michael@0: } michael@0: } michael@0: } michael@0: michael@0: LOG("Iterating the actual nodes of the window palette"); michael@0: for (let node of aWindowPalette.children) { michael@0: LOG("In palette children: " + node.id); michael@0: if (node.id && !this.getPlacementOfWidget(node.id)) { michael@0: widgets.add(node.id); michael@0: } michael@0: } michael@0: michael@0: return [...widgets]; michael@0: }, michael@0: michael@0: getPlacementOfWidget: function(aWidgetId, aOnlyRegistered, aDeadAreas) { michael@0: if (aOnlyRegistered && !this.widgetExists(aWidgetId)) { michael@0: return null; michael@0: } michael@0: michael@0: for (let [area, placements] of gPlacements) { michael@0: if (!gAreas.has(area) && !aDeadAreas) { michael@0: continue; michael@0: } michael@0: let index = placements.indexOf(aWidgetId); michael@0: if (index != -1) { michael@0: return { area: area, position: index }; michael@0: } michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: widgetExists: function(aWidgetId) { michael@0: if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) { michael@0: return true; michael@0: } michael@0: michael@0: // Destroyed API widgets are in gSeenWidgets, but not in gPalette: michael@0: if (gSeenWidgets.has(aWidgetId)) { michael@0: return false; michael@0: } michael@0: michael@0: // We're assuming XUL widgets always exist, as it's much harder to check, michael@0: // and checking would be much more error prone. michael@0: return true; michael@0: }, michael@0: michael@0: addWidgetToArea: function(aWidgetId, aArea, aPosition, aInitialAdd) { michael@0: if (!gAreas.has(aArea)) { michael@0: throw new Error("Unknown customization area: " + aArea); michael@0: } michael@0: michael@0: // Hack: don't want special widgets in the panel (need to check here as well michael@0: // as in canWidgetMoveToArea because the menu panel is lazy): michael@0: if (gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL && michael@0: this.isSpecialWidget(aWidgetId)) { michael@0: return; michael@0: } michael@0: michael@0: // If this is a lazy area that hasn't been restored yet, we can't yet modify michael@0: // it - would would at least like to add to it. So we keep track of it in michael@0: // gFuturePlacements, and use that to add it when restoring the area. We michael@0: // throw away aPosition though, as that can only be bogus if the area hasn't michael@0: // yet been restorted (caller can't possibly know where its putting the michael@0: // widget in relation to other widgets). michael@0: if (this.isAreaLazy(aArea)) { michael@0: gFuturePlacements.get(aArea).add(aWidgetId); michael@0: return; michael@0: } michael@0: michael@0: if (this.isSpecialWidget(aWidgetId)) { michael@0: aWidgetId = this.ensureSpecialWidgetId(aWidgetId); michael@0: } michael@0: michael@0: let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true); michael@0: if (oldPlacement && oldPlacement.area == aArea) { michael@0: this.moveWidgetWithinArea(aWidgetId, aPosition); michael@0: return; michael@0: } michael@0: michael@0: // Do nothing if the widget is not allowed to move to the target area. michael@0: if (!this.canWidgetMoveToArea(aWidgetId, aArea)) { michael@0: return; michael@0: } michael@0: michael@0: if (oldPlacement) { michael@0: this.removeWidgetFromArea(aWidgetId); michael@0: } michael@0: michael@0: if (!gPlacements.has(aArea)) { michael@0: gPlacements.set(aArea, [aWidgetId]); michael@0: aPosition = 0; michael@0: } else { michael@0: let placements = gPlacements.get(aArea); michael@0: if (typeof aPosition != "number") { michael@0: aPosition = placements.length; michael@0: } michael@0: if (aPosition < 0) { michael@0: aPosition = 0; michael@0: } michael@0: placements.splice(aPosition, 0, aWidgetId); michael@0: } michael@0: michael@0: let widget = gPalette.get(aWidgetId); michael@0: if (widget) { michael@0: widget.currentArea = aArea; michael@0: widget.currentPosition = aPosition; michael@0: } michael@0: michael@0: // We initially set placements with addWidgetToArea, so in that case michael@0: // we don't consider the area "dirtied". michael@0: if (!aInitialAdd) { michael@0: gDirtyAreaCache.add(aArea); michael@0: } michael@0: michael@0: gDirty = true; michael@0: this.saveState(); michael@0: michael@0: this.notifyListeners("onWidgetAdded", aWidgetId, aArea, aPosition); michael@0: }, michael@0: michael@0: removeWidgetFromArea: function(aWidgetId) { michael@0: let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true); michael@0: if (!oldPlacement) { michael@0: return; michael@0: } michael@0: michael@0: if (!this.isWidgetRemovable(aWidgetId)) { michael@0: return; michael@0: } michael@0: michael@0: let placements = gPlacements.get(oldPlacement.area); michael@0: let position = placements.indexOf(aWidgetId); michael@0: if (position != -1) { michael@0: placements.splice(position, 1); michael@0: } michael@0: michael@0: let widget = gPalette.get(aWidgetId); michael@0: if (widget) { michael@0: widget.currentArea = null; michael@0: widget.currentPosition = null; michael@0: } michael@0: michael@0: gDirty = true; michael@0: this.saveState(); michael@0: gDirtyAreaCache.add(oldPlacement.area); michael@0: michael@0: this.notifyListeners("onWidgetRemoved", aWidgetId, oldPlacement.area); michael@0: }, michael@0: michael@0: moveWidgetWithinArea: function(aWidgetId, aPosition) { michael@0: let oldPlacement = this.getPlacementOfWidget(aWidgetId); michael@0: if (!oldPlacement) { michael@0: return; michael@0: } michael@0: michael@0: let placements = gPlacements.get(oldPlacement.area); michael@0: if (typeof aPosition != "number") { michael@0: aPosition = placements.length; michael@0: } else if (aPosition < 0) { michael@0: aPosition = 0; michael@0: } else if (aPosition > placements.length) { michael@0: aPosition = placements.length; michael@0: } michael@0: michael@0: let widget = gPalette.get(aWidgetId); michael@0: if (widget) { michael@0: widget.currentPosition = aPosition; michael@0: widget.currentArea = oldPlacement.area; michael@0: } michael@0: michael@0: if (aPosition == oldPlacement.position) { michael@0: return; michael@0: } michael@0: michael@0: placements.splice(oldPlacement.position, 1); michael@0: // If we just removed the item from *before* where it is now added, michael@0: // we need to compensate the position offset for that: michael@0: if (oldPlacement.position < aPosition) { michael@0: aPosition--; michael@0: } michael@0: placements.splice(aPosition, 0, aWidgetId); michael@0: michael@0: gDirty = true; michael@0: gDirtyAreaCache.add(oldPlacement.area); michael@0: michael@0: this.saveState(); michael@0: michael@0: this.notifyListeners("onWidgetMoved", aWidgetId, oldPlacement.area, michael@0: oldPlacement.position, aPosition); michael@0: }, michael@0: michael@0: // Note that this does not populate gPlacements, which is done lazily so that michael@0: // the legacy state can be migrated, which is only available once a browser michael@0: // window is openned. michael@0: // The panel area is an exception here, since it has no legacy state and is michael@0: // built lazily - and therefore wouldn't otherwise result in restoring its michael@0: // state immediately when a browser window opens, which is important for michael@0: // other consumers of this API. michael@0: loadSavedState: function() { michael@0: let state = null; michael@0: try { michael@0: state = Services.prefs.getCharPref(kPrefCustomizationState); michael@0: } catch (e) { michael@0: LOG("No saved state found"); michael@0: // This will fail if nothing has been customized, so silently fall back to michael@0: // the defaults. michael@0: } michael@0: michael@0: if (!state) { michael@0: return; michael@0: } michael@0: try { michael@0: gSavedState = JSON.parse(state); michael@0: if (typeof gSavedState != "object" || gSavedState === null) { michael@0: throw "Invalid saved state"; michael@0: } michael@0: } catch(e) { michael@0: Services.prefs.clearUserPref(kPrefCustomizationState); michael@0: gSavedState = {}; michael@0: LOG("Error loading saved UI customization state, falling back to defaults."); michael@0: } michael@0: michael@0: if (!("placements" in gSavedState)) { michael@0: gSavedState.placements = {}; michael@0: } michael@0: michael@0: gSeenWidgets = new Set(gSavedState.seen || []); michael@0: gDirtyAreaCache = new Set(gSavedState.dirtyAreaCache || []); michael@0: gNewElementCount = gSavedState.newElementCount || 0; michael@0: }, michael@0: michael@0: restoreStateForArea: function(aArea, aLegacyState) { michael@0: let placementsPreexisted = gPlacements.has(aArea); michael@0: michael@0: this.beginBatchUpdate(); michael@0: try { michael@0: gRestoring = true; michael@0: michael@0: let restored = false; michael@0: if (placementsPreexisted) { michael@0: LOG("Restoring " + aArea + " from pre-existing placements"); michael@0: for (let [position, id] in Iterator(gPlacements.get(aArea))) { michael@0: this.moveWidgetWithinArea(id, position); michael@0: } michael@0: gDirty = false; michael@0: restored = true; michael@0: } else { michael@0: gPlacements.set(aArea, []); michael@0: } michael@0: michael@0: if (!restored && gSavedState && aArea in gSavedState.placements) { michael@0: LOG("Restoring " + aArea + " from saved state"); michael@0: let placements = gSavedState.placements[aArea]; michael@0: for (let id of placements) michael@0: this.addWidgetToArea(id, aArea); michael@0: gDirty = false; michael@0: restored = true; michael@0: } michael@0: michael@0: if (!restored && aLegacyState) { michael@0: LOG("Restoring " + aArea + " from legacy state"); michael@0: for (let id of aLegacyState) michael@0: this.addWidgetToArea(id, aArea); michael@0: // Don't override dirty state, to ensure legacy state is saved here and michael@0: // therefore only used once. michael@0: restored = true; michael@0: } michael@0: michael@0: if (!restored) { michael@0: LOG("Restoring " + aArea + " from default state"); michael@0: let defaults = gAreas.get(aArea).get("defaultPlacements"); michael@0: if (defaults) { michael@0: for (let id of defaults) michael@0: this.addWidgetToArea(id, aArea, null, true); michael@0: } michael@0: gDirty = false; michael@0: } michael@0: michael@0: // Finally, add widgets to the area that were added before the it was able michael@0: // to be restored. This can occur when add-ons register widgets for a michael@0: // lazily-restored area before it's been restored. michael@0: if (gFuturePlacements.has(aArea)) { michael@0: for (let id of gFuturePlacements.get(aArea)) michael@0: this.addWidgetToArea(id, aArea); michael@0: } michael@0: michael@0: LOG("Placements for " + aArea + ":\n\t" + gPlacements.get(aArea).join("\n\t")); michael@0: michael@0: gRestoring = false; michael@0: } finally { michael@0: this.endBatchUpdate(); michael@0: } michael@0: }, michael@0: michael@0: saveState: function() { michael@0: if (gInBatchStack || !gDirty) { michael@0: return; michael@0: } michael@0: let state = { placements: gPlacements, michael@0: seen: gSeenWidgets, michael@0: dirtyAreaCache: gDirtyAreaCache, michael@0: newElementCount: gNewElementCount }; michael@0: michael@0: LOG("Saving state."); michael@0: let serialized = JSON.stringify(state, this.serializerHelper); michael@0: LOG("State saved as: " + serialized); michael@0: Services.prefs.setCharPref(kPrefCustomizationState, serialized); michael@0: gDirty = false; michael@0: }, michael@0: michael@0: serializerHelper: function(aKey, aValue) { michael@0: if (typeof aValue == "object" && aValue.constructor.name == "Map") { michael@0: let result = {}; michael@0: for (let [mapKey, mapValue] of aValue) michael@0: result[mapKey] = mapValue; michael@0: return result; michael@0: } michael@0: michael@0: if (typeof aValue == "object" && aValue.constructor.name == "Set") { michael@0: return [...aValue]; michael@0: } michael@0: michael@0: return aValue; michael@0: }, michael@0: michael@0: beginBatchUpdate: function() { michael@0: gInBatchStack++; michael@0: }, michael@0: michael@0: endBatchUpdate: function(aForceDirty) { michael@0: gInBatchStack--; michael@0: if (aForceDirty === true) { michael@0: gDirty = true; michael@0: } michael@0: if (gInBatchStack == 0) { michael@0: this.saveState(); michael@0: } else if (gInBatchStack < 0) { michael@0: throw new Error("The batch editing stack should never reach a negative number."); michael@0: } michael@0: }, michael@0: michael@0: addListener: function(aListener) { michael@0: gListeners.add(aListener); michael@0: }, michael@0: michael@0: removeListener: function(aListener) { michael@0: if (aListener == this) { michael@0: return; michael@0: } michael@0: michael@0: gListeners.delete(aListener); michael@0: }, michael@0: michael@0: notifyListeners: function(aEvent, ...aArgs) { michael@0: if (gRestoring) { michael@0: return; michael@0: } michael@0: michael@0: for (let listener of gListeners) { michael@0: try { michael@0: if (typeof listener[aEvent] == "function") { michael@0: listener[aEvent].apply(listener, aArgs); michael@0: } michael@0: } catch (e) { michael@0: ERROR(e + " -- " + e.fileName + ":" + e.lineNumber); michael@0: } michael@0: } michael@0: }, michael@0: michael@0: _dispatchToolboxEventToWindow: function(aEventType, aDetails, aWindow) { michael@0: let evt = new aWindow.CustomEvent(aEventType, { michael@0: bubbles: true, michael@0: cancelable: true, michael@0: detail: aDetails michael@0: }); michael@0: aWindow.gNavToolbox.dispatchEvent(evt); michael@0: }, michael@0: michael@0: dispatchToolboxEvent: function(aEventType, aDetails={}, aWindow=null) { michael@0: if (aWindow) { michael@0: return this._dispatchToolboxEventToWindow(aEventType, aDetails, aWindow); michael@0: } michael@0: for (let [win, ] of gBuildWindows) { michael@0: this._dispatchToolboxEventToWindow(aEventType, aDetails, win); michael@0: } michael@0: }, michael@0: michael@0: createWidget: function(aProperties) { michael@0: let widget = this.normalizeWidget(aProperties, CustomizableUI.SOURCE_EXTERNAL); michael@0: //XXXunf This should probably throw. michael@0: if (!widget) { michael@0: return; michael@0: } michael@0: michael@0: gPalette.set(widget.id, widget); michael@0: michael@0: // Clear our caches: michael@0: gGroupWrapperCache.delete(widget.id); michael@0: for (let [win, ] of gBuildWindows) { michael@0: let cache = gSingleWrapperCache.get(win); michael@0: if (cache) { michael@0: cache.delete(widget.id); michael@0: } michael@0: } michael@0: michael@0: this.notifyListeners("onWidgetCreated", widget.id); michael@0: michael@0: if (widget.defaultArea) { michael@0: let area = gAreas.get(widget.defaultArea); michael@0: //XXXgijs this won't have any effect for legacy items. Sort of OK because michael@0: // consumers can modify currentset? Maybe? michael@0: if (area.has("defaultPlacements")) { michael@0: area.get("defaultPlacements").push(widget.id); michael@0: } else { michael@0: area.set("defaultPlacements", [widget.id]); michael@0: } michael@0: } michael@0: michael@0: // Look through previously saved state to see if we're restoring a widget. michael@0: let seenAreas = new Set(); michael@0: let widgetMightNeedAutoAdding = true; michael@0: for (let [area, placements] of gPlacements) { michael@0: seenAreas.add(area); michael@0: let areaIsRegistered = gAreas.has(area); michael@0: let index = gPlacements.get(area).indexOf(widget.id); michael@0: if (index != -1) { michael@0: widgetMightNeedAutoAdding = false; michael@0: if (areaIsRegistered) { michael@0: widget.currentArea = area; michael@0: widget.currentPosition = index; michael@0: } michael@0: break; michael@0: } michael@0: } michael@0: michael@0: // Also look at saved state data directly in areas that haven't yet been michael@0: // restored. Can't rely on this for restored areas, as they may have michael@0: // changed. michael@0: if (widgetMightNeedAutoAdding && gSavedState) { michael@0: for (let area of Object.keys(gSavedState.placements)) { michael@0: if (seenAreas.has(area)) { michael@0: continue; michael@0: } michael@0: michael@0: let areaIsRegistered = gAreas.has(area); michael@0: let index = gSavedState.placements[area].indexOf(widget.id); michael@0: if (index != -1) { michael@0: widgetMightNeedAutoAdding = false; michael@0: if (areaIsRegistered) { michael@0: widget.currentArea = area; michael@0: widget.currentPosition = index; michael@0: } michael@0: break; michael@0: } michael@0: } michael@0: } michael@0: michael@0: // If we're restoring the widget to it's old placement, fire off the michael@0: // onWidgetAdded event - our own handler will take care of adding it to michael@0: // any build areas. michael@0: if (widget.currentArea) { michael@0: this.notifyListeners("onWidgetAdded", widget.id, widget.currentArea, michael@0: widget.currentPosition); michael@0: } else if (widgetMightNeedAutoAdding) { michael@0: let autoAdd = true; michael@0: try { michael@0: autoAdd = Services.prefs.getBoolPref(kPrefCustomizationAutoAdd); michael@0: } catch (e) {} michael@0: michael@0: // If the widget doesn't have an existing placement, and it hasn't been michael@0: // seen before, then add it to its default area so it can be used. michael@0: // If the widget is not removable, we *have* to add it to its default michael@0: // area here. michael@0: let canBeAutoAdded = autoAdd && !gSeenWidgets.has(widget.id); michael@0: if (!widget.currentArea && (!widget.removable || canBeAutoAdded)) { michael@0: this.beginBatchUpdate(); michael@0: try { michael@0: gSeenWidgets.add(widget.id); michael@0: michael@0: if (widget.defaultArea) { michael@0: if (this.isAreaLazy(widget.defaultArea)) { michael@0: gFuturePlacements.get(widget.defaultArea).add(widget.id); michael@0: } else { michael@0: this.addWidgetToArea(widget.id, widget.defaultArea); michael@0: } michael@0: } michael@0: } finally { michael@0: this.endBatchUpdate(true); michael@0: } michael@0: } michael@0: } michael@0: michael@0: this.notifyListeners("onWidgetAfterCreation", widget.id, widget.currentArea); michael@0: return widget.id; michael@0: }, michael@0: michael@0: createBuiltinWidget: function(aData) { michael@0: // This should only ever be called on startup, before any windows are michael@0: // opened - so we know there's no build areas to handle. Also, builtin michael@0: // widgets are expected to be (mostly) static, so shouldn't affect the michael@0: // current placement settings. michael@0: let widget = this.normalizeWidget(aData, CustomizableUI.SOURCE_BUILTIN); michael@0: if (!widget) { michael@0: ERROR("Error creating builtin widget: " + aData.id); michael@0: return; michael@0: } michael@0: michael@0: LOG("Creating built-in widget with id: " + widget.id); michael@0: gPalette.set(widget.id, widget); michael@0: }, michael@0: michael@0: // Returns true if the area will eventually lazily restore (but hasn't yet). michael@0: isAreaLazy: function(aArea) { michael@0: if (gPlacements.has(aArea)) { michael@0: return false; michael@0: } michael@0: return gAreas.get(aArea).has("legacy"); michael@0: }, michael@0: michael@0: //XXXunf Log some warnings here, when the data provided isn't up to scratch. michael@0: normalizeWidget: function(aData, aSource) { michael@0: let widget = { michael@0: implementation: aData, michael@0: source: aSource || "addon", michael@0: instances: new Map(), michael@0: currentArea: null, michael@0: removable: true, michael@0: overflows: true, michael@0: defaultArea: null, michael@0: shortcutId: null, michael@0: tooltiptext: null, michael@0: showInPrivateBrowsing: true, michael@0: }; michael@0: michael@0: if (typeof aData.id != "string" || !/^[a-z0-9-_]{1,}$/i.test(aData.id)) { michael@0: ERROR("Given an illegal id in normalizeWidget: " + aData.id); michael@0: return null; michael@0: } michael@0: michael@0: delete widget.implementation.currentArea; michael@0: widget.implementation.__defineGetter__("currentArea", function() widget.currentArea); michael@0: michael@0: const kReqStringProps = ["id"]; michael@0: for (let prop of kReqStringProps) { michael@0: if (typeof aData[prop] != "string") { michael@0: ERROR("Missing required property '" + prop + "' in normalizeWidget: " michael@0: + aData.id); michael@0: return null; michael@0: } michael@0: widget[prop] = aData[prop]; michael@0: } michael@0: michael@0: const kOptStringProps = ["label", "tooltiptext", "shortcutId"]; michael@0: for (let prop of kOptStringProps) { michael@0: if (typeof aData[prop] == "string") { michael@0: widget[prop] = aData[prop]; michael@0: } michael@0: } michael@0: michael@0: const kOptBoolProps = ["removable", "showInPrivateBrowsing", "overflows"]; michael@0: for (let prop of kOptBoolProps) { michael@0: if (typeof aData[prop] == "boolean") { michael@0: widget[prop] = aData[prop]; michael@0: } michael@0: } michael@0: michael@0: if (aData.defaultArea && gAreas.has(aData.defaultArea)) { michael@0: widget.defaultArea = aData.defaultArea; michael@0: } else if (!widget.removable) { michael@0: ERROR("Widget '" + widget.id + "' is not removable but does not specify " + michael@0: "a valid defaultArea. That's not possible; it must specify a " + michael@0: "valid defaultArea as well."); michael@0: return null; michael@0: } michael@0: michael@0: if ("type" in aData && gSupportedWidgetTypes.has(aData.type)) { michael@0: widget.type = aData.type; michael@0: } else { michael@0: widget.type = "button"; michael@0: } michael@0: michael@0: widget.disabled = aData.disabled === true; michael@0: michael@0: this.wrapWidgetEventHandler("onBeforeCreated", widget); michael@0: this.wrapWidgetEventHandler("onClick", widget); michael@0: this.wrapWidgetEventHandler("onCreated", widget); michael@0: michael@0: if (widget.type == "button") { michael@0: widget.onCommand = typeof aData.onCommand == "function" ? michael@0: aData.onCommand : michael@0: null; michael@0: } else if (widget.type == "view") { michael@0: if (typeof aData.viewId != "string") { michael@0: ERROR("Expected a string for widget " + widget.id + " viewId, but got " michael@0: + aData.viewId); michael@0: return null; michael@0: } michael@0: widget.viewId = aData.viewId; michael@0: michael@0: this.wrapWidgetEventHandler("onViewShowing", widget); michael@0: this.wrapWidgetEventHandler("onViewHiding", widget); michael@0: } else if (widget.type == "custom") { michael@0: this.wrapWidgetEventHandler("onBuild", widget); michael@0: } michael@0: michael@0: if (gPalette.has(widget.id)) { michael@0: return null; michael@0: } michael@0: michael@0: return widget; michael@0: }, michael@0: michael@0: wrapWidgetEventHandler: function(aEventName, aWidget) { michael@0: if (typeof aWidget.implementation[aEventName] != "function") { michael@0: aWidget[aEventName] = null; michael@0: return; michael@0: } michael@0: aWidget[aEventName] = function(...aArgs) { michael@0: // Wrap inside a try...catch to properly log errors, until bug 862627 is michael@0: // fixed, which in turn might help bug 503244. michael@0: try { michael@0: // Don't copy the function to the normalized widget object, instead michael@0: // keep it on the original object provided to the API so that michael@0: // additional methods can be implemented and used by the event michael@0: // handlers. michael@0: return aWidget.implementation[aEventName].apply(aWidget.implementation, michael@0: aArgs); michael@0: } catch (e) { michael@0: Cu.reportError(e); michael@0: } michael@0: }; michael@0: }, michael@0: michael@0: destroyWidget: function(aWidgetId) { michael@0: let widget = gPalette.get(aWidgetId); michael@0: if (!widget) { michael@0: gGroupWrapperCache.delete(aWidgetId); michael@0: for (let [window, ] of gBuildWindows) { michael@0: let windowCache = gSingleWrapperCache.get(window); michael@0: if (windowCache) { michael@0: windowCache.delete(aWidgetId); michael@0: } michael@0: } michael@0: return; michael@0: } michael@0: michael@0: // Remove it from the default placements of an area if it was added there: michael@0: if (widget.defaultArea) { michael@0: let area = gAreas.get(widget.defaultArea); michael@0: if (area) { michael@0: let defaultPlacements = area.get("defaultPlacements"); michael@0: // We can assume this is present because if a widget has a defaultArea, michael@0: // we automatically create a defaultPlacements array for that area. michael@0: let widgetIndex = defaultPlacements.indexOf(aWidgetId); michael@0: if (widgetIndex != -1) { michael@0: defaultPlacements.splice(widgetIndex, 1); michael@0: } michael@0: } michael@0: } michael@0: michael@0: // This will not remove the widget from gPlacements - we want to keep the michael@0: // setting so the widget gets put back in it's old position if/when it michael@0: // returns. michael@0: for (let [window, ] of gBuildWindows) { michael@0: let windowCache = gSingleWrapperCache.get(window); michael@0: if (windowCache) { michael@0: windowCache.delete(aWidgetId); michael@0: } michael@0: let widgetNode = window.document.getElementById(aWidgetId) || michael@0: window.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0]; michael@0: if (widgetNode) { michael@0: let container = widgetNode.parentNode michael@0: this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null, michael@0: container, true); michael@0: widgetNode.remove(); michael@0: this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null, michael@0: container, true); michael@0: } michael@0: if (widget.type == "view") { michael@0: let viewNode = window.document.getElementById(widget.viewId); michael@0: if (viewNode) { michael@0: for (let eventName of kSubviewEvents) { michael@0: let handler = "on" + eventName; michael@0: if (typeof widget[handler] == "function") { michael@0: viewNode.removeEventListener(eventName, widget[handler], false); michael@0: } michael@0: } michael@0: } michael@0: } michael@0: } michael@0: michael@0: gPalette.delete(aWidgetId); michael@0: gGroupWrapperCache.delete(aWidgetId); michael@0: michael@0: this.notifyListeners("onWidgetDestroyed", aWidgetId); michael@0: }, michael@0: michael@0: getCustomizeTargetForArea: function(aArea, aWindow) { michael@0: let buildAreaNodes = gBuildAreas.get(aArea); michael@0: if (!buildAreaNodes) { michael@0: return null; michael@0: } michael@0: michael@0: for (let node of buildAreaNodes) { michael@0: if (node.ownerDocument.defaultView === aWindow) { michael@0: return node.customizationTarget ? node.customizationTarget : node; michael@0: } michael@0: } michael@0: michael@0: return null; michael@0: }, michael@0: michael@0: reset: function() { michael@0: gResetting = true; michael@0: this._resetUIState(); michael@0: michael@0: // Rebuild each registered area (across windows) to reflect the state that michael@0: // was reset above. michael@0: this._rebuildRegisteredAreas(); michael@0: michael@0: gResetting = false; michael@0: }, michael@0: michael@0: _resetUIState: function() { michael@0: try { michael@0: gUIStateBeforeReset.drawInTitlebar = Services.prefs.getBoolPref(kPrefDrawInTitlebar); michael@0: gUIStateBeforeReset.uiCustomizationState = Services.prefs.getCharPref(kPrefCustomizationState); michael@0: } catch(e) { } michael@0: michael@0: this._resetExtraToolbars(); michael@0: michael@0: Services.prefs.clearUserPref(kPrefCustomizationState); michael@0: Services.prefs.clearUserPref(kPrefDrawInTitlebar); michael@0: LOG("State reset"); michael@0: michael@0: // Reset placements to make restoring default placements possible. michael@0: gPlacements = new Map(); michael@0: gDirtyAreaCache = new Set(); michael@0: gSeenWidgets = new Set(); michael@0: // Clear the saved state to ensure that defaults will be used. michael@0: gSavedState = null; michael@0: // Restore the state for each area to its defaults michael@0: for (let [areaId,] of gAreas) { michael@0: this.restoreStateForArea(areaId); michael@0: } michael@0: }, michael@0: michael@0: _resetExtraToolbars: function(aFilter = null) { michael@0: let firstWindow = true; // Only need to unregister and persist once michael@0: for (let [win, ] of gBuildWindows) { michael@0: let toolbox = win.gNavToolbox; michael@0: for (let child of toolbox.children) { michael@0: let matchesFilter = !aFilter || aFilter == child.id; michael@0: if (child.hasAttribute("customindex") && matchesFilter) { michael@0: let toolbarId = "toolbar" + child.getAttribute("customindex"); michael@0: toolbox.toolbarset.removeAttribute(toolbarId); michael@0: if (firstWindow) { michael@0: win.document.persist(toolbox.toolbarset.id, toolbarId); michael@0: // We have to unregister it properly to ensure we don't kill michael@0: // XUL widgets which might be in here michael@0: this.unregisterArea(child.id, true); michael@0: } michael@0: child.remove(); michael@0: } michael@0: } michael@0: firstWindow = false; michael@0: } michael@0: }, michael@0: michael@0: _rebuildRegisteredAreas: function() { michael@0: for (let [areaId, areaNodes] of gBuildAreas) { michael@0: let placements = gPlacements.get(areaId); michael@0: let isFirstChangedToolbar = true; michael@0: for (let areaNode of areaNodes) { michael@0: this.buildArea(areaId, placements, areaNode); michael@0: michael@0: let area = gAreas.get(areaId); michael@0: if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) { michael@0: let defaultCollapsed = area.get("defaultCollapsed"); michael@0: let win = areaNode.ownerDocument.defaultView; michael@0: if (defaultCollapsed !== null) { michael@0: win.setToolbarVisibility(areaNode, !defaultCollapsed, isFirstChangedToolbar); michael@0: } michael@0: } michael@0: isFirstChangedToolbar = false; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Undoes a previous reset, restoring the state of the UI to the state prior to the reset. michael@0: */ michael@0: undoReset: function() { michael@0: if (gUIStateBeforeReset.uiCustomizationState == null || michael@0: gUIStateBeforeReset.drawInTitlebar == null) { michael@0: return; michael@0: } michael@0: gUndoResetting = true; michael@0: michael@0: let uiCustomizationState = gUIStateBeforeReset.uiCustomizationState; michael@0: let drawInTitlebar = gUIStateBeforeReset.drawInTitlebar; michael@0: michael@0: // Need to clear the previous state before setting the prefs michael@0: // because pref observers may check if there is a previous UI state. michael@0: this._clearPreviousUIState(); michael@0: michael@0: Services.prefs.setCharPref(kPrefCustomizationState, uiCustomizationState); michael@0: Services.prefs.setBoolPref(kPrefDrawInTitlebar, drawInTitlebar); michael@0: this.loadSavedState(); michael@0: // If the user just customizes toolbar/titlebar visibility, gSavedState will be null michael@0: // and we don't need to do anything else here: michael@0: if (gSavedState) { michael@0: for (let areaId of Object.keys(gSavedState.placements)) { michael@0: let placements = gSavedState.placements[areaId]; michael@0: gPlacements.set(areaId, placements); michael@0: } michael@0: this._rebuildRegisteredAreas(); michael@0: } michael@0: michael@0: gUndoResetting = false; michael@0: }, michael@0: michael@0: _clearPreviousUIState: function() { michael@0: Object.getOwnPropertyNames(gUIStateBeforeReset).forEach((prop) => { michael@0: gUIStateBeforeReset[prop] = null; michael@0: }); michael@0: }, michael@0: michael@0: removeExtraToolbar: function(aToolbarId) { michael@0: this._resetExtraToolbars(aToolbarId); michael@0: }, michael@0: michael@0: /** michael@0: * @param {String|Node} aWidget - widget ID or a widget node (preferred for performance). michael@0: * @return {Boolean} whether the widget is removable michael@0: */ michael@0: isWidgetRemovable: function(aWidget) { michael@0: let widgetId; michael@0: let widgetNode; michael@0: if (typeof aWidget == "string") { michael@0: widgetId = aWidget; michael@0: } else { michael@0: widgetId = aWidget.id; michael@0: widgetNode = aWidget; michael@0: } michael@0: let provider = this.getWidgetProvider(widgetId); michael@0: michael@0: if (provider == CustomizableUI.PROVIDER_API) { michael@0: return gPalette.get(widgetId).removable; michael@0: } michael@0: michael@0: if (provider == CustomizableUI.PROVIDER_XUL) { michael@0: if (gBuildWindows.size == 0) { michael@0: // We don't have any build windows to look at, so just assume for now michael@0: // that its removable. michael@0: return true; michael@0: } michael@0: michael@0: if (!widgetNode) { michael@0: // Pick any of the build windows to look at. michael@0: let [window,] = [...gBuildWindows][0]; michael@0: [, widgetNode] = this.getWidgetNode(widgetId, window); michael@0: } michael@0: // If we don't have a node, we assume it's removable. This can happen because michael@0: // getWidgetProvider returns PROVIDER_XUL by default, but this will also happen michael@0: // for API-provided widgets which have been destroyed. michael@0: if (!widgetNode) { michael@0: return true; michael@0: } michael@0: return widgetNode.getAttribute("removable") == "true"; michael@0: } michael@0: michael@0: // Otherwise this is either a special widget, which is always removable, or michael@0: // an API widget which has already been removed from gPalette. Returning true michael@0: // here allows us to then remove its ID from any placements where it might michael@0: // still occur. michael@0: return true; michael@0: }, michael@0: michael@0: canWidgetMoveToArea: function(aWidgetId, aArea) { michael@0: let placement = this.getPlacementOfWidget(aWidgetId); michael@0: if (placement && placement.area != aArea) { michael@0: // Special widgets can't move to the menu panel. michael@0: if (this.isSpecialWidget(aWidgetId) && gAreas.has(aArea) && michael@0: gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL) { michael@0: return false; michael@0: } michael@0: // For everything else, just return whether the widget is removable. michael@0: return this.isWidgetRemovable(aWidgetId); michael@0: } michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: ensureWidgetPlacedInWindow: function(aWidgetId, aWindow) { michael@0: let placement = this.getPlacementOfWidget(aWidgetId); michael@0: if (!placement) { michael@0: return false; michael@0: } michael@0: let areaNodes = gBuildAreas.get(placement.area); michael@0: if (!areaNodes) { michael@0: return false; michael@0: } michael@0: let container = [...areaNodes].filter((n) => n.ownerDocument.defaultView == aWindow); michael@0: if (!container.length) { michael@0: return false; michael@0: } michael@0: let existingNode = container[0].getElementsByAttribute("id", aWidgetId)[0]; michael@0: if (existingNode) { michael@0: return true; michael@0: } michael@0: michael@0: this.insertNodeInWindow(aWidgetId, container[0], true); michael@0: return true; michael@0: }, michael@0: michael@0: get inDefaultState() { michael@0: for (let [areaId, props] of gAreas) { michael@0: let defaultPlacements = props.get("defaultPlacements"); michael@0: // Areas without default placements (like legacy ones?) get skipped michael@0: if (!defaultPlacements) { michael@0: continue; michael@0: } michael@0: michael@0: let currentPlacements = gPlacements.get(areaId); michael@0: // We're excluding all of the placement IDs for items that do not exist, michael@0: // and items that have removable="false", michael@0: // because we don't want to consider them when determining if we're michael@0: // in the default state. This way, if an add-on introduces a widget michael@0: // and is then uninstalled, the leftover placement doesn't cause us to michael@0: // automatically assume that the buttons are not in the default state. michael@0: let buildAreaNodes = gBuildAreas.get(areaId); michael@0: if (buildAreaNodes && buildAreaNodes.size) { michael@0: let container = [...buildAreaNodes][0]; michael@0: let removableOrDefault = (itemNodeOrItem) => { michael@0: let item = (itemNodeOrItem && itemNodeOrItem.id) || itemNodeOrItem; michael@0: let isRemovable = this.isWidgetRemovable(itemNodeOrItem); michael@0: let isInDefault = defaultPlacements.indexOf(item) != -1; michael@0: return isRemovable || isInDefault; michael@0: }; michael@0: // Toolbars have a currentSet property which also deals correctly with overflown michael@0: // widgets (if any) - use that instead: michael@0: if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { michael@0: let currentSet = container.currentSet; michael@0: currentPlacements = currentSet ? currentSet.split(',') : []; michael@0: currentPlacements = currentPlacements.filter(removableOrDefault); michael@0: } else { michael@0: // Clone the array so we don't modify the actual placements... michael@0: currentPlacements = [...currentPlacements]; michael@0: currentPlacements = currentPlacements.filter((item) => { michael@0: let itemNode = container.getElementsByAttribute("id", item)[0]; michael@0: return itemNode && removableOrDefault(itemNode || item); michael@0: }); michael@0: } michael@0: michael@0: if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { michael@0: let attribute = container.getAttribute("type") == "menubar" ? "autohide" : "collapsed"; michael@0: let collapsed = container.getAttribute(attribute) == "true"; michael@0: let defaultCollapsed = props.get("defaultCollapsed"); michael@0: if (defaultCollapsed !== null && collapsed != defaultCollapsed) { michael@0: LOG("Found " + areaId + " had non-default toolbar visibility (expected " + defaultCollapsed + ", was " + collapsed + ")"); michael@0: return false; michael@0: } michael@0: } michael@0: } michael@0: LOG("Checking default state for " + areaId + ":\n" + currentPlacements.join(",") + michael@0: "\nvs.\n" + defaultPlacements.join(",")); michael@0: michael@0: if (currentPlacements.length != defaultPlacements.length) { michael@0: return false; michael@0: } michael@0: michael@0: for (let i = 0; i < currentPlacements.length; ++i) { michael@0: if (currentPlacements[i] != defaultPlacements[i]) { michael@0: LOG("Found " + currentPlacements[i] + " in " + areaId + " where " + michael@0: defaultPlacements[i] + " was expected!"); michael@0: return false; michael@0: } michael@0: } michael@0: } michael@0: michael@0: if (Services.prefs.prefHasUserValue(kPrefDrawInTitlebar)) { michael@0: LOG(kPrefDrawInTitlebar + " pref is non-default"); michael@0: return false; michael@0: } michael@0: michael@0: return true; michael@0: }, michael@0: michael@0: setToolbarVisibility: function(aToolbarId, aIsVisible) { michael@0: // We only persist the attribute the first time. michael@0: let isFirstChangedToolbar = true; michael@0: for (let window of CustomizableUI.windows) { michael@0: let toolbar = window.document.getElementById(aToolbarId); michael@0: if (toolbar) { michael@0: window.setToolbarVisibility(toolbar, aIsVisible, isFirstChangedToolbar); michael@0: isFirstChangedToolbar = false; michael@0: } michael@0: } michael@0: }, michael@0: }; michael@0: Object.freeze(CustomizableUIInternal); michael@0: michael@0: this.CustomizableUI = { michael@0: /** michael@0: * Constant reference to the ID of the menu panel. michael@0: */ michael@0: get AREA_PANEL() "PanelUI-contents", michael@0: /** michael@0: * Constant reference to the ID of the navigation toolbar. michael@0: */ michael@0: get AREA_NAVBAR() "nav-bar", michael@0: /** michael@0: * Constant reference to the ID of the menubar's toolbar. michael@0: */ michael@0: get AREA_MENUBAR() "toolbar-menubar", michael@0: /** michael@0: * Constant reference to the ID of the tabstrip toolbar. michael@0: */ michael@0: get AREA_TABSTRIP() "TabsToolbar", michael@0: /** michael@0: * Constant reference to the ID of the bookmarks toolbar. michael@0: */ michael@0: get AREA_BOOKMARKS() "PersonalToolbar", michael@0: /** michael@0: * Constant reference to the ID of the addon-bar toolbar shim. michael@0: * Do not use, this will be removed as soon as reasonably possible. michael@0: * @deprecated michael@0: */ michael@0: get AREA_ADDONBAR() "addon-bar", michael@0: /** michael@0: * Constant indicating the area is a menu panel. michael@0: */ michael@0: get TYPE_MENU_PANEL() "menu-panel", michael@0: /** michael@0: * Constant indicating the area is a toolbar. michael@0: */ michael@0: get TYPE_TOOLBAR() "toolbar", michael@0: michael@0: /** michael@0: * Constant indicating a XUL-type provider. michael@0: */ michael@0: get PROVIDER_XUL() "xul", michael@0: /** michael@0: * Constant indicating an API-type provider. michael@0: */ michael@0: get PROVIDER_API() "api", michael@0: /** michael@0: * Constant indicating dynamic (special) widgets: spring, spacer, and separator. michael@0: */ michael@0: get PROVIDER_SPECIAL() "special", michael@0: michael@0: /** michael@0: * Constant indicating the widget is built-in michael@0: */ michael@0: get SOURCE_BUILTIN() "builtin", michael@0: /** michael@0: * Constant indicating the widget is externally provided michael@0: * (e.g. by add-ons or other items not part of the builtin widget set). michael@0: */ michael@0: get SOURCE_EXTERNAL() "external", michael@0: michael@0: /** michael@0: * The class used to distinguish items that span the entire menu panel. michael@0: */ michael@0: get WIDE_PANEL_CLASS() "panel-wide-item", michael@0: /** michael@0: * The (constant) number of columns in the menu panel. michael@0: */ michael@0: get PANEL_COLUMN_COUNT() 3, michael@0: michael@0: /** michael@0: * Constant indicating the reason the event was fired was a window closing michael@0: */ michael@0: get REASON_WINDOW_CLOSED() "window-closed", michael@0: /** michael@0: * Constant indicating the reason the event was fired was an area being michael@0: * unregistered separately from window closing mechanics. michael@0: */ michael@0: get REASON_AREA_UNREGISTERED() "area-unregistered", michael@0: michael@0: michael@0: /** michael@0: * An iteratable property of windows managed by CustomizableUI. michael@0: * Note that this can *only* be used as an iterator. ie: michael@0: * for (let window of CustomizableUI.windows) { ... } michael@0: */ michael@0: windows: { michael@0: "@@iterator": function*() { michael@0: for (let [window,] of gBuildWindows) michael@0: yield window; michael@0: } michael@0: }, michael@0: michael@0: /** michael@0: * Add a listener object that will get fired for various events regarding michael@0: * customization. michael@0: * michael@0: * @param aListener the listener object to add michael@0: * michael@0: * Not all event handler methods need to be defined. michael@0: * CustomizableUI will catch exceptions. Events are dispatched michael@0: * synchronously on the UI thread, so if you can delay any/some of your michael@0: * processing, that is advisable. The following event handlers are supported: michael@0: * - onWidgetAdded(aWidgetId, aArea, aPosition) michael@0: * Fired when a widget is added to an area. aWidgetId is the widget that michael@0: * was added, aArea the area it was added to, and aPosition the position michael@0: * in which it was added. michael@0: * - onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition) michael@0: * Fired when a widget is moved within its area. aWidgetId is the widget michael@0: * that was moved, aArea the area it was moved in, aOldPosition its old michael@0: * position, and aNewPosition its new position. michael@0: * - onWidgetRemoved(aWidgetId, aArea) michael@0: * Fired when a widget is removed from its area. aWidgetId is the widget michael@0: * that was removed, aArea the area it was removed from. michael@0: * michael@0: * - onWidgetBeforeDOMChange(aNode, aNextNode, aContainer, aIsRemoval) michael@0: * Fired *before* a widget's DOM node is acted upon by CustomizableUI michael@0: * (to add, move or remove it). aNode is the DOM node changed, aNextNode michael@0: * the DOM node (if any) before which a widget will be inserted, michael@0: * aContainer the *actual* DOM container (could be an overflow panel in michael@0: * case of an overflowable toolbar), and aWasRemoval is true iff the michael@0: * action about to happen is the removal of the DOM node. michael@0: * - onWidgetAfterDOMChange(aNode, aNextNode, aContainer, aWasRemoval) michael@0: * Like onWidgetBeforeDOMChange, but fired after the change to the DOM michael@0: * node of the widget. michael@0: * michael@0: * - onWidgetReset(aNode, aContainer) michael@0: * Fired after a reset to default placements moves a widget's node to a michael@0: * different location. aNode is the widget's node, aContainer is the michael@0: * area it was moved into (NB: it might already have been there and been michael@0: * moved to a different position!) michael@0: * - onWidgetUndoMove(aNode, aContainer) michael@0: * Fired after undoing a reset to default placements moves a widget's michael@0: * node to a different location. aNode is the widget's node, aContainer michael@0: * is the area it was moved into (NB: it might already have been there michael@0: * and been moved to a different position!) michael@0: * - onAreaReset(aArea, aContainer) michael@0: * Fired after a reset to default placements is complete on an area's michael@0: * DOM node. Note that this is fired for each DOM node. aArea is the area michael@0: * that was reset, aContainer the DOM node that was reset. michael@0: * michael@0: * - onWidgetCreated(aWidgetId) michael@0: * Fired when a widget with id aWidgetId has been created, but before it michael@0: * is added to any placements or any DOM nodes have been constructed. michael@0: * Only fired for API-based widgets. michael@0: * - onWidgetAfterCreation(aWidgetId, aArea) michael@0: * Fired after a widget with id aWidgetId has been created, and has been michael@0: * added to either its default area or the area in which it was placed michael@0: * previously. If the widget has no default area and/or it has never michael@0: * been placed anywhere, aArea may be null. Only fired for API-based michael@0: * widgets. michael@0: * - onWidgetDestroyed(aWidgetId) michael@0: * Fired when widgets are destroyed. aWidgetId is the widget that is michael@0: * being destroyed. Only fired for API-based widgets. michael@0: * - onWidgetInstanceRemoved(aWidgetId, aDocument) michael@0: * Fired when a window is unloaded and a widget's instance is destroyed michael@0: * because of this. Only fired for API-based widgets. michael@0: * michael@0: * - onWidgetDrag(aWidgetId, aArea) michael@0: * Fired both when and after customize mode drag handling system tries michael@0: * to determine the width and height of widget aWidgetId when dragged to a michael@0: * different area. aArea will be the area the item is dragged to, or michael@0: * undefined after the measurements have been done and the node has been michael@0: * moved back to its 'regular' area. michael@0: * michael@0: * - onCustomizeStart(aWindow) michael@0: * Fired when opening customize mode in aWindow. michael@0: * - onCustomizeEnd(aWindow) michael@0: * Fired when exiting customize mode in aWindow. michael@0: * michael@0: * - onWidgetOverflow(aNode, aContainer) michael@0: * Fired when a widget's DOM node is overflowing its container, a toolbar, michael@0: * and will be displayed in the overflow panel. michael@0: * - onWidgetUnderflow(aNode, aContainer) michael@0: * Fired when a widget's DOM node is *not* overflowing its container, a michael@0: * toolbar, anymore. michael@0: * - onWindowOpened(aWindow) michael@0: * Fired when a window has been opened that is managed by CustomizableUI, michael@0: * once all of the prerequisite setup has been done. michael@0: * - onWindowClosed(aWindow) michael@0: * Fired when a window that has been managed by CustomizableUI has been michael@0: * closed. michael@0: * - onAreaNodeRegistered(aArea, aContainer) michael@0: * Fired after an area node is first built when it is registered. This michael@0: * is often when the window has opened, but in the case of add-ons, michael@0: * could fire when the node has just been registered with CustomizableUI michael@0: * after an add-on update or disable/enable sequence. michael@0: * - onAreaNodeUnregistered(aArea, aContainer, aReason) michael@0: * Fired when an area node is explicitly unregistered by an API caller, michael@0: * or by a window closing. The aReason parameter indicates which of michael@0: * these is the case. michael@0: */ michael@0: addListener: function(aListener) { michael@0: CustomizableUIInternal.addListener(aListener); michael@0: }, michael@0: /** michael@0: * Remove a listener added with addListener michael@0: * @param aListener the listener object to remove michael@0: */ michael@0: removeListener: function(aListener) { michael@0: CustomizableUIInternal.removeListener(aListener); michael@0: }, michael@0: michael@0: /** michael@0: * Register a customizable area with CustomizableUI. michael@0: * @param aName the name of the area to register. Can only contain michael@0: * alphanumeric characters, dashes (-) and underscores (_). michael@0: * @param aProps the properties of the area. The following properties are michael@0: * recognized: michael@0: * - type: the type of area. Either TYPE_TOOLBAR (default) or michael@0: * TYPE_MENU_PANEL; michael@0: * - anchor: for a menu panel or overflowable toolbar, the michael@0: * anchoring node for the panel. michael@0: * - legacy: set to true if you want customizableui to michael@0: * automatically migrate the currentset attribute michael@0: * - overflowable: set to true if your toolbar is overflowable. michael@0: * This requires an anchor, and only has an michael@0: * effect for toolbars. michael@0: * - defaultPlacements: an array of widget IDs making up the michael@0: * default contents of the area michael@0: * - defaultCollapsed: (INTERNAL ONLY) applies if the type is TYPE_TOOLBAR, specifies michael@0: * if toolbar is collapsed by default (default to true). michael@0: * Specify null to ensure that reset/inDefaultArea don't care michael@0: * about a toolbar's collapsed state michael@0: */ michael@0: registerArea: function(aName, aProperties) { michael@0: CustomizableUIInternal.registerArea(aName, aProperties); michael@0: }, michael@0: /** michael@0: * Register a concrete node for a registered area. This method is automatically michael@0: * called from any toolbar in the main browser window that has its michael@0: * "customizable" attribute set to true. There should normally be no need to michael@0: * call it yourself. michael@0: * michael@0: * Note that ideally, you should register your toolbar using registerArea michael@0: * before any of the toolbars have their XBL bindings constructed (which michael@0: * will happen when they're added to the DOM and are not hidden). If you michael@0: * don't, and your toolbar has a defaultset attribute, CustomizableUI will michael@0: * register it automatically. If your toolbar does not have a defaultset michael@0: * attribute, the node will be saved for processing when you call michael@0: * registerArea. Note that CustomizableUI won't restore state in the area, michael@0: * allow the user to customize it in customize mode, or otherwise deal michael@0: * with it, until the area has been registered. michael@0: */ michael@0: registerToolbarNode: function(aToolbar, aExistingChildren) { michael@0: CustomizableUIInternal.registerToolbarNode(aToolbar, aExistingChildren); michael@0: }, michael@0: /** michael@0: * Register the menu panel node. This method should not be called by anyone michael@0: * apart from the built-in PanelUI. michael@0: * @param aPanel the panel DOM node being registered. michael@0: */ michael@0: registerMenuPanel: function(aPanel) { michael@0: CustomizableUIInternal.registerMenuPanel(aPanel); michael@0: }, michael@0: /** michael@0: * Unregister a customizable area. The inverse of registerArea. michael@0: * michael@0: * Unregistering an area will remove all the (removable) widgets in the michael@0: * area, which will return to the panel, and destroy all other traces michael@0: * of the area within CustomizableUI. Note that this means the *contents* michael@0: * of the area's DOM nodes will be moved to the panel or removed, but michael@0: * the area's DOM nodes *themselves* will stay. michael@0: * michael@0: * Furthermore, by default the placements of the area will be kept in the michael@0: * saved state (!) and restored if you re-register the area at a later michael@0: * point. This is useful for e.g. add-ons that get disabled and then michael@0: * re-enabled (e.g. when they update). michael@0: * michael@0: * You can override this last behaviour (and destroy the placements michael@0: * information in the saved state) by passing true for aDestroyPlacements. michael@0: * michael@0: * @param aName the name of the area to unregister michael@0: * @param aDestroyPlacements whether to destroy the placements information michael@0: * for the area, too. michael@0: */ michael@0: unregisterArea: function(aName, aDestroyPlacements) { michael@0: CustomizableUIInternal.unregisterArea(aName, aDestroyPlacements); michael@0: }, michael@0: /** michael@0: * Add a widget to an area. michael@0: * If the area to which you try to add is not known to CustomizableUI, michael@0: * this will throw. michael@0: * If the area to which you try to add has not yet been restored from its michael@0: * legacy state, this will postpone the addition. michael@0: * If the area to which you try to add is the same as the area in which michael@0: * the widget is currently placed, this will do the same as michael@0: * moveWidgetWithinArea. michael@0: * If the widget cannot be removed from its original location, this will michael@0: * no-op. michael@0: * michael@0: * This will fire an onWidgetAdded notification, michael@0: * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification michael@0: * for each window CustomizableUI knows about. michael@0: * michael@0: * @param aWidgetId the ID of the widget to add michael@0: * @param aArea the ID of the area to add the widget to michael@0: * @param aPosition the position at which to add the widget. If you do not michael@0: * pass a position, the widget will be added to the end michael@0: * of the area. michael@0: */ michael@0: addWidgetToArea: function(aWidgetId, aArea, aPosition) { michael@0: CustomizableUIInternal.addWidgetToArea(aWidgetId, aArea, aPosition); michael@0: }, michael@0: /** michael@0: * Remove a widget from its area. If the widget cannot be removed from its michael@0: * area, or is not in any area, this will no-op. Otherwise, this will fire an michael@0: * onWidgetRemoved notification, and an onWidgetBeforeDOMChange and michael@0: * onWidgetAfterDOMChange notification for each window CustomizableUI knows michael@0: * about. michael@0: * michael@0: * @param aWidgetId the ID of the widget to remove michael@0: */ michael@0: removeWidgetFromArea: function(aWidgetId) { michael@0: CustomizableUIInternal.removeWidgetFromArea(aWidgetId); michael@0: }, michael@0: /** michael@0: * Move a widget within an area. michael@0: * If the widget is not in any area, this will no-op. michael@0: * If the widget is already at the indicated position, this will no-op. michael@0: * michael@0: * Otherwise, this will move the widget and fire an onWidgetMoved notification, michael@0: * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification for michael@0: * each window CustomizableUI knows about. michael@0: * michael@0: * @param aWidgetId the ID of the widget to move michael@0: * @param aPosition the position to move the widget to. michael@0: * Negative values or values greater than the number of michael@0: * widgets will be interpreted to mean moving the widget to michael@0: * respectively the first or last position. michael@0: */ michael@0: moveWidgetWithinArea: function(aWidgetId, aPosition) { michael@0: CustomizableUIInternal.moveWidgetWithinArea(aWidgetId, aPosition); michael@0: }, michael@0: /** michael@0: * Ensure a XUL-based widget created in a window after areas were michael@0: * initialized moves to its correct position. michael@0: * This is roughly equivalent to manually looking up the position and using michael@0: * insertItem in the old API, but a lot less work for consumers. michael@0: * Always prefer this over using toolbar.insertItem (which might no-op michael@0: * because it delegates to addWidgetToArea) or, worse, moving items in the michael@0: * DOM yourself. michael@0: * michael@0: * @param aWidgetId the ID of the widget that was just created michael@0: * @param aWindow the window in which you want to ensure it was added. michael@0: * michael@0: * NB: why is this API per-window, you wonder? Because if you need this, michael@0: * presumably you yourself need to create the widget in all the windows michael@0: * and need to loop through them anyway. michael@0: */ michael@0: ensureWidgetPlacedInWindow: function(aWidgetId, aWindow) { michael@0: return CustomizableUIInternal.ensureWidgetPlacedInWindow(aWidgetId, aWindow); michael@0: }, michael@0: /** michael@0: * Start a batch update of items. michael@0: * During a batch update, the customization state is not saved to the user's michael@0: * preferences file, in order to reduce (possibly sync) IO. michael@0: * Calls to begin/endBatchUpdate may be nested. michael@0: * michael@0: * Callers should ensure that NO MATTER WHAT they call endBatchUpdate once michael@0: * for each call to beginBatchUpdate, even if there are exceptions in the michael@0: * code in the batch update. Otherwise, for the duration of the michael@0: * Firefox session, customization state is never saved. Typically, you michael@0: * would do this using a try...finally block. michael@0: */ michael@0: beginBatchUpdate: function() { michael@0: CustomizableUIInternal.beginBatchUpdate(); michael@0: }, michael@0: /** michael@0: * End a batch update. See the documentation for beginBatchUpdate above. michael@0: * michael@0: * State is not saved if we believe it is identical to the last known michael@0: * saved state. State is only ever saved when all batch updates have michael@0: * finished (ie there has been 1 endBatchUpdate call for each michael@0: * beginBatchUpdate call). If any of the endBatchUpdate calls pass michael@0: * aForceDirty=true, we will flush to the prefs file. michael@0: * michael@0: * @param aForceDirty force CustomizableUI to flush to the prefs file when michael@0: * all batch updates have finished. michael@0: */ michael@0: endBatchUpdate: function(aForceDirty) { michael@0: CustomizableUIInternal.endBatchUpdate(aForceDirty); michael@0: }, michael@0: /** michael@0: * Create a widget. michael@0: * michael@0: * To create a widget, you should pass an object with its desired michael@0: * properties. The following properties are supported: michael@0: * michael@0: * - id: the ID of the widget (required). michael@0: * - type: a string indicating the type of widget. Possible types michael@0: * are: michael@0: * 'button' - for simple button widgets (the default) michael@0: * 'view' - for buttons that open a panel or subview, michael@0: * depending on where they are placed. michael@0: * 'custom' - for fine-grained control over the creation michael@0: * of the widget. michael@0: * - viewId: Only useful for views (and required there): the id of the michael@0: * that should be shown when clicking the widget. michael@0: * - onBuild(aDoc): Only useful for custom widgets (and required there); a michael@0: * function that will be invoked with the document in which michael@0: * to build a widget. Should return the DOM node that has michael@0: * been constructed. michael@0: * - onBeforeCreated(aDoc): Attached to all non-custom widgets; a function michael@0: * that will be invoked before the widget gets a DOM node michael@0: * constructed, passing the document in which that will happen. michael@0: * This is useful especially for 'view' type widgets that need michael@0: * to construct their views on the fly (e.g. from bootstrapped michael@0: * add-ons) michael@0: * - onCreated(aNode): Attached to all widgets; a function that will be invoked michael@0: * whenever the widget has a DOM node constructed, passing the michael@0: * constructed node as an argument. michael@0: * - onCommand(aEvt): Only useful for button widgets; a function that will be michael@0: * invoked when the user activates the button. michael@0: * - onClick(aEvt): Attached to all widgets; a function that will be invoked michael@0: * when the user clicks the widget. michael@0: * - onViewShowing(aEvt): Only useful for views; a function that will be michael@0: * invoked when a user shows your view. michael@0: * - onViewHiding(aEvt): Only useful for views; a function that will be michael@0: * invoked when a user hides your view. michael@0: * - tooltiptext: string to use for the tooltip of the widget michael@0: * - label: string to use for the label of the widget michael@0: * - removable: whether the widget is removable (optional, default: true) michael@0: * NB: if you specify false here, you must provide a michael@0: * defaultArea, too. michael@0: * - overflows: whether widget can overflow when in an overflowable michael@0: * toolbar (optional, default: true) michael@0: * - defaultArea: default area to add the widget to michael@0: * (optional, default: none; required if non-removable) michael@0: * - shortcutId: id of an element that has a shortcut for this widget michael@0: * (optional, default: null). This is only used to display michael@0: * the shortcut as part of the tooltip for builtin widgets michael@0: * (which have strings inside michael@0: * customizableWidgets.properties). If you're in an add-on, michael@0: * you should not set this property. michael@0: * - showInPrivateBrowsing: whether to show the widget in private browsing michael@0: * mode (optional, default: true) michael@0: * michael@0: * @param aProperties the specifications for the widget. michael@0: * @return a wrapper around the created widget (see getWidget) michael@0: */ michael@0: createWidget: function(aProperties) { michael@0: return CustomizableUIInternal.wrapWidget( michael@0: CustomizableUIInternal.createWidget(aProperties) michael@0: ); michael@0: }, michael@0: /** michael@0: * Destroy a widget michael@0: * michael@0: * If the widget is part of the default placements in an area, this will michael@0: * remove it from there. It will also remove any DOM instances. However, michael@0: * it will keep the widget in the placements for whatever area it was michael@0: * in at the time. You can remove it from there yourself by calling michael@0: * CustomizableUI.removeWidgetFromArea(aWidgetId). michael@0: * michael@0: * @param aWidgetId the ID of the widget to destroy michael@0: */ michael@0: destroyWidget: function(aWidgetId) { michael@0: CustomizableUIInternal.destroyWidget(aWidgetId); michael@0: }, michael@0: /** michael@0: * Get a wrapper object with information about the widget. michael@0: * The object provides the following properties michael@0: * (all read-only unless otherwise indicated): michael@0: * michael@0: * - id: the widget's ID; michael@0: * - type: the type of widget (button, view, custom). For michael@0: * XUL-provided widgets, this is always 'custom'; michael@0: * - provider: the provider type of the widget, id est one of michael@0: * PROVIDER_API or PROVIDER_XUL; michael@0: * - forWindow(w): a method to obtain a single window wrapper for a widget, michael@0: * in the window w passed as the only argument; michael@0: * - instances: an array of all instances (single window wrappers) michael@0: * of the widget. This array is NOT live; michael@0: * - areaType: the type of the widget's current area michael@0: * - isGroup: true; will be false for wrappers around single widget nodes; michael@0: * - source: for API-provided widgets, whether they are built-in to michael@0: * Firefox or add-on-provided; michael@0: * - disabled: for API-provided widgets, whether the widget is currently michael@0: * disabled. NB: this property is writable, and will toggle michael@0: * all the widgets' nodes' disabled states; michael@0: * - label: for API-provied widgets, the label of the widget; michael@0: * - tooltiptext: for API-provided widgets, the tooltip of the widget; michael@0: * - showInPrivateBrowsing: for API-provided widgets, whether the widget is michael@0: * visible in private browsing; michael@0: * michael@0: * Single window wrappers obtained through forWindow(someWindow) or from the michael@0: * instances array have the following properties michael@0: * (all read-only unless otherwise indicated): michael@0: * michael@0: * - id: the widget's ID; michael@0: * - type: the type of widget (button, view, custom). For michael@0: * XUL-provided widgets, this is always 'custom'; michael@0: * - provider: the provider type of the widget, id est one of michael@0: * PROVIDER_API or PROVIDER_XUL; michael@0: * - node: reference to the corresponding DOM node; michael@0: * - anchor: the anchor on which to anchor panels opened from this michael@0: * node. This will point to the overflow chevron on michael@0: * overflowable toolbars if and only if your widget node michael@0: * is overflowed, to the anchor for the panel menu michael@0: * if your widget is inside the panel menu, and to the michael@0: * node itself in all other cases; michael@0: * - overflowed: boolean indicating whether the node is currently in the michael@0: * overflow panel of the toolbar; michael@0: * - isGroup: false; will be true for the group widget; michael@0: * - label: for API-provided widgets, convenience getter for the michael@0: * label attribute of the DOM node; michael@0: * - tooltiptext: for API-provided widgets, convenience getter for the michael@0: * tooltiptext attribute of the DOM node; michael@0: * - disabled: for API-provided widgets, convenience getter *and setter* michael@0: * for the disabled state of this single widget. Note that michael@0: * you may prefer to use the group wrapper's getter/setter michael@0: * instead. michael@0: * michael@0: * @param aWidgetId the ID of the widget whose information you need michael@0: * @return a wrapper around the widget as described above, or null if the michael@0: * widget is known not to exist (anymore). NB: non-null return michael@0: * is no guarantee the widget exists because we cannot know in michael@0: * advance if a XUL widget exists or not. michael@0: */ michael@0: getWidget: function(aWidgetId) { michael@0: return CustomizableUIInternal.wrapWidget(aWidgetId); michael@0: }, michael@0: /** michael@0: * Get an array of widget wrappers (see getWidget) for all the widgets michael@0: * which are currently not in any area (so which are in the palette). michael@0: * michael@0: * @param aWindowPalette the palette (and by extension, the window) in which michael@0: * CustomizableUI should look. This matters because of michael@0: * course XUL-provided widgets could be available in michael@0: * some windows but not others, and likewise michael@0: * API-provided widgets might not exist in a private michael@0: * window (because of the showInPrivateBrowsing michael@0: * property). michael@0: * michael@0: * @return an array of widget wrappers (see getWidget) michael@0: */ michael@0: getUnusedWidgets: function(aWindowPalette) { michael@0: return CustomizableUIInternal.getUnusedWidgets(aWindowPalette).map( michael@0: CustomizableUIInternal.wrapWidget, michael@0: CustomizableUIInternal michael@0: ); michael@0: }, michael@0: /** michael@0: * Get an array of all the widget IDs placed in an area. This is roughly michael@0: * equivalent to fetching the currentset attribute and splitting by commas michael@0: * in the legacy APIs. Modifying the array will not affect CustomizableUI. michael@0: * michael@0: * @param aArea the ID of the area whose placements you want to obtain. michael@0: * @return an array containing the widget IDs that are in the area. michael@0: * michael@0: * NB: will throw if called too early (before placements have been fetched) michael@0: * or if the area is not currently known to CustomizableUI. michael@0: */ michael@0: getWidgetIdsInArea: function(aArea) { michael@0: if (!gAreas.has(aArea)) { michael@0: throw new Error("Unknown customization area: " + aArea); michael@0: } michael@0: if (!gPlacements.has(aArea)) { michael@0: throw new Error("Area not yet restored"); michael@0: } michael@0: michael@0: // We need to clone this, as we don't want to let consumers muck with placements michael@0: return [...gPlacements.get(aArea)]; michael@0: }, michael@0: /** michael@0: * Get an array of widget wrappers for all the widgets in an area. This is michael@0: * the same as calling getWidgetIdsInArea and .map() ing the result through michael@0: * CustomizableUI.getWidget. Careful: this means that if there are IDs in there michael@0: * which don't have corresponding DOM nodes (like in the old-style currentset michael@0: * attribute), there might be nulls in this array, or items for which michael@0: * wrapper.forWindow(win) will return null. michael@0: * michael@0: * @param aArea the ID of the area whose widgets you want to obtain. michael@0: * @return an array of widget wrappers and/or null values for the widget IDs michael@0: * placed in an area. michael@0: * michael@0: * NB: will throw if called too early (before placements have been fetched) michael@0: * or if the area is not currently known to CustomizableUI. michael@0: */ michael@0: getWidgetsInArea: function(aArea) { michael@0: return this.getWidgetIdsInArea(aArea).map( michael@0: CustomizableUIInternal.wrapWidget, michael@0: CustomizableUIInternal michael@0: ); michael@0: }, michael@0: /** michael@0: * Obtain an array of all the area IDs known to CustomizableUI. michael@0: * This array is created for you, so is modifiable without CustomizableUI michael@0: * being affected. michael@0: */ michael@0: get areas() { michael@0: return [area for ([area, props] of gAreas)]; michael@0: }, michael@0: /** michael@0: * Check what kind of area (toolbar or menu panel) an area is. This is michael@0: * useful if you have a widget that needs to behave differently depending michael@0: * on its location. Note that widget wrappers have a convenience getter michael@0: * property (areaType) for this purpose. michael@0: * michael@0: * @param aArea the ID of the area whose type you want to know michael@0: * @return TYPE_TOOLBAR or TYPE_MENU_PANEL depending on the area, null if michael@0: * the area is unknown. michael@0: */ michael@0: getAreaType: function(aArea) { michael@0: let area = gAreas.get(aArea); michael@0: return area ? area.get("type") : null; michael@0: }, michael@0: /** michael@0: * Check if a toolbar is collapsed by default. michael@0: * michael@0: * @param aArea the ID of the area whose default-collapsed state you want to know. michael@0: * @return `true` or `false` depending on the area, null if the area is unknown, michael@0: * or its collapsed state cannot normally be controlled by the user michael@0: */ michael@0: isToolbarDefaultCollapsed: function(aArea) { michael@0: let area = gAreas.get(aArea); michael@0: return area ? area.get("defaultCollapsed") : null; michael@0: }, michael@0: /** michael@0: * Obtain the DOM node that is the customize target for an area in a michael@0: * specific window. michael@0: * michael@0: * Areas can have a customization target that does not correspond to the michael@0: * node itself. In particular, toolbars that have a customizationtarget michael@0: * attribute set will have their customization target set to that node. michael@0: * This means widgets will end up in the customization target, not in the michael@0: * DOM node with the ID that corresponds to the area ID. This is useful michael@0: * because it lets you have fixed content in a toolbar (e.g. the panel michael@0: * menu item in the navbar) and have all the customizable widgets use michael@0: * the customization target. michael@0: * michael@0: * Using this API yourself is discouraged; you should generally not need michael@0: * to be asking for the DOM container node used for a particular area. michael@0: * In particular, if you're wanting to check it in relation to a widget's michael@0: * node, your DOM node might not be a direct child of the customize target michael@0: * in a window if, for instance, the window is in customization mode, or if michael@0: * this is an overflowable toolbar and the widget has been overflowed. michael@0: * michael@0: * @param aArea the ID of the area whose customize target you want to have michael@0: * @param aWindow the window where you want to fetch the DOM node. michael@0: * @return the customize target DOM node for aArea in aWindow michael@0: */ michael@0: getCustomizeTargetForArea: function(aArea, aWindow) { michael@0: return CustomizableUIInternal.getCustomizeTargetForArea(aArea, aWindow); michael@0: }, michael@0: /** michael@0: * Reset the customization state back to its default. michael@0: * michael@0: * This is the nuclear option. You should never call this except if the user michael@0: * explicitly requests it. Firefox does this when the user clicks the michael@0: * "Restore Defaults" button in customize mode. michael@0: */ michael@0: reset: function() { michael@0: CustomizableUIInternal.reset(); michael@0: }, michael@0: michael@0: /** michael@0: * Undo the previous reset, can only be called immediately after a reset. michael@0: * @return a promise that will be resolved when the operation is complete. michael@0: */ michael@0: undoReset: function() { michael@0: CustomizableUIInternal.undoReset(); michael@0: }, michael@0: michael@0: /** michael@0: * Remove a custom toolbar added in a previous version of Firefox or using michael@0: * an add-on. NB: only works on the customizable toolbars generated by michael@0: * the toolbox itself. Intended for use from CustomizeMode, not by michael@0: * other consumers. michael@0: * @param aToolbarId the ID of the toolbar to remove michael@0: */ michael@0: removeExtraToolbar: function(aToolbarId) { michael@0: CustomizableUIInternal.removeExtraToolbar(aToolbarId); michael@0: }, michael@0: michael@0: /** michael@0: * Can the last Restore Defaults operation be undone. michael@0: * michael@0: * @return A boolean stating whether an undo of the michael@0: * Restore Defaults can be performed. michael@0: */ michael@0: get canUndoReset() { michael@0: return gUIStateBeforeReset.uiCustomizationState != null || michael@0: gUIStateBeforeReset.drawInTitlebar != null; michael@0: }, michael@0: michael@0: /** michael@0: * Get the placement of a widget. This is by far the best way to obtain michael@0: * information about what the state of your widget is. The internals of michael@0: * this call are cheap (no DOM necessary) and you will know where the user michael@0: * has put your widget. michael@0: * michael@0: * @param aWidgetId the ID of the widget whose placement you want to know michael@0: * @return michael@0: * { michael@0: * area: "somearea", // The ID of the area where the widget is placed michael@0: * position: 42 // the index in the placements array corresponding to michael@0: * // your widget. michael@0: * } michael@0: * michael@0: * OR michael@0: * michael@0: * null // if the widget is not placed anywhere (ie in the palette) michael@0: */ michael@0: getPlacementOfWidget: function(aWidgetId) { michael@0: return CustomizableUIInternal.getPlacementOfWidget(aWidgetId, true); michael@0: }, michael@0: /** michael@0: * Check if a widget can be removed from the area it's in. michael@0: * michael@0: * Note that if you're wanting to move the widget somewhere, you should michael@0: * generally be checking canWidgetMoveToArea, because that will return michael@0: * true if the widget is already in the area where you want to move it (!). michael@0: * michael@0: * NB: oh, also, this method might lie if the widget in question is a michael@0: * XUL-provided widget and there are no windows open, because it michael@0: * can obviously not check anything in this case. It will return michael@0: * true. You will be able to move the widget elsewhere. However, michael@0: * once the user reopens a window, the widget will move back to its michael@0: * 'proper' area automagically. michael@0: * michael@0: * @param aWidgetId a widget ID or DOM node to check michael@0: * @return true if the widget can be removed from its area, michael@0: * false otherwise. michael@0: */ michael@0: isWidgetRemovable: function(aWidgetId) { michael@0: return CustomizableUIInternal.isWidgetRemovable(aWidgetId); michael@0: }, michael@0: /** michael@0: * Check if a widget can be moved to a particular area. Like michael@0: * isWidgetRemovable but better, because it'll return true if the widget michael@0: * is already in the right area. michael@0: * michael@0: * @param aWidgetId the widget ID or DOM node you want to move somewhere michael@0: * @param aArea the area ID you want to move it to. michael@0: * @return true if this is possible, false if it is not. The same caveats as michael@0: * for isWidgetRemovable apply, however, if no windows are open. michael@0: */ michael@0: canWidgetMoveToArea: function(aWidgetId, aArea) { michael@0: return CustomizableUIInternal.canWidgetMoveToArea(aWidgetId, aArea); michael@0: }, michael@0: /** michael@0: * Whether we're in a default state. Note that non-removable non-default michael@0: * widgets and non-existing widgets are not taken into account in determining michael@0: * whether we're in the default state. michael@0: * michael@0: * NB: this is a property with a getter. The getter is NOT cheap, because michael@0: * it does smart things with non-removable non-default items, non-existent michael@0: * items, and so forth. Please don't call unless necessary. michael@0: */ michael@0: get inDefaultState() { michael@0: return CustomizableUIInternal.inDefaultState; michael@0: }, michael@0: michael@0: /** michael@0: * Set a toolbar's visibility state in all windows. michael@0: * @param aToolbarId the toolbar whose visibility should be adjusted michael@0: * @param aIsVisible whether the toolbar should be visible michael@0: */ michael@0: setToolbarVisibility: function(aToolbarId, aIsVisible) { michael@0: CustomizableUIInternal.setToolbarVisibility(aToolbarId, aIsVisible); michael@0: }, michael@0: michael@0: /** michael@0: * Get a localized property off a (widget?) object. michael@0: * michael@0: * NB: this is unlikely to be useful unless you're in Firefox code, because michael@0: * this code uses the builtin widget stringbundle, and can't be told michael@0: * to use add-on-provided strings. It's mainly here as convenience for michael@0: * custom builtin widgets that build their own DOM but use the same michael@0: * stringbundle as the other builtin widgets. michael@0: * michael@0: * @param aWidget the object whose property we should use to fetch a michael@0: * localizable string; michael@0: * @param aProp the property on the object to use for the fetching; michael@0: * @param aFormatArgs (optional) any extra arguments to use for a formatted michael@0: * string; michael@0: * @param aDef (optional) the default to return if we don't find the michael@0: * string in the stringbundle; michael@0: * michael@0: * @return the localized string, or aDef if the string isn't in the bundle. michael@0: * If no default is provided, michael@0: * if aProp exists on aWidget, we'll return that, michael@0: * otherwise we'll return the empty string michael@0: * michael@0: */ michael@0: getLocalizedProperty: function(aWidget, aProp, aFormatArgs, aDef) { michael@0: return CustomizableUIInternal.getLocalizedProperty(aWidget, aProp, michael@0: aFormatArgs, aDef); michael@0: }, michael@0: /** michael@0: * Given a node, walk up to the first panel in its ancestor chain, and michael@0: * close it. michael@0: * michael@0: * @param aNode a node whose panel should be closed; michael@0: */ michael@0: hidePanelForNode: function(aNode) { michael@0: CustomizableUIInternal.hidePanelForNode(aNode); michael@0: }, michael@0: /** michael@0: * Check if a widget is a "special" widget: a spring, spacer or separator. michael@0: * michael@0: * @param aWidgetId the widget ID to check. michael@0: * @return true if the widget is 'special', false otherwise. michael@0: */ michael@0: isSpecialWidget: function(aWidgetId) { michael@0: return CustomizableUIInternal.isSpecialWidget(aWidgetId); michael@0: }, michael@0: /** michael@0: * Add listeners to a panel that will close it. For use from the menu panel michael@0: * and overflowable toolbar implementations, unlikely to be useful for michael@0: * consumers. michael@0: * michael@0: * @param aPanel the panel to which listeners should be attached. michael@0: */ michael@0: addPanelCloseListeners: function(aPanel) { michael@0: CustomizableUIInternal.addPanelCloseListeners(aPanel); michael@0: }, michael@0: /** michael@0: * Remove close listeners that have been added to a panel with michael@0: * addPanelCloseListeners. For use from the menu panel and overflowable michael@0: * toolbar implementations, unlikely to be useful for consumers. michael@0: * michael@0: * @param aPanel the panel from which listeners should be removed. michael@0: */ michael@0: removePanelCloseListeners: function(aPanel) { michael@0: CustomizableUIInternal.removePanelCloseListeners(aPanel); michael@0: }, michael@0: /** michael@0: * Notify listeners a widget is about to be dragged to an area. For use from michael@0: * Customize Mode only, do not use otherwise. michael@0: * michael@0: * @param aWidgetId the ID of the widget that is being dragged to an area. michael@0: * @param aArea the ID of the area to which the widget is being dragged. michael@0: */ michael@0: onWidgetDrag: function(aWidgetId, aArea) { michael@0: CustomizableUIInternal.notifyListeners("onWidgetDrag", aWidgetId, aArea); michael@0: }, michael@0: /** michael@0: * Notify listeners that a window is entering customize mode. For use from michael@0: * Customize Mode only, do not use otherwise. michael@0: * @param aWindow the window entering customize mode michael@0: */ michael@0: notifyStartCustomizing: function(aWindow) { michael@0: CustomizableUIInternal.notifyListeners("onCustomizeStart", aWindow); michael@0: }, michael@0: /** michael@0: * Notify listeners that a window is exiting customize mode. For use from michael@0: * Customize Mode only, do not use otherwise. michael@0: * @param aWindow the window exiting customize mode michael@0: */ michael@0: notifyEndCustomizing: function(aWindow) { michael@0: CustomizableUIInternal.notifyListeners("onCustomizeEnd", aWindow); michael@0: }, michael@0: michael@0: /** michael@0: * Notify toolbox(es) of a particular event. If you don't pass aWindow, michael@0: * all toolboxes will be notified. For use from Customize Mode only, michael@0: * do not use otherwise. michael@0: * @param aEvent the name of the event to send. michael@0: * @param aDetails optional, the details of the event. michael@0: * @param aWindow optional, the window in which to send the event. michael@0: */ michael@0: dispatchToolboxEvent: function(aEvent, aDetails={}, aWindow=null) { michael@0: CustomizableUIInternal.dispatchToolboxEvent(aEvent, aDetails, aWindow); michael@0: }, michael@0: michael@0: /** michael@0: * Check whether an area is overflowable. michael@0: * michael@0: * @param aAreaId the ID of an area to check for overflowable-ness michael@0: * @return true if the area is overflowable, false otherwise. michael@0: */ michael@0: isAreaOverflowable: function(aAreaId) { michael@0: let area = gAreas.get(aAreaId); michael@0: return area ? area.get("type") == this.TYPE_TOOLBAR && area.get("overflowable") michael@0: : false; michael@0: }, michael@0: /** michael@0: * Obtain a string indicating the place of an element. This is intended michael@0: * for use from customize mode; You should generally use getPlacementOfWidget michael@0: * instead, which is cheaper because it does not use the DOM. michael@0: * michael@0: * @param aElement the DOM node whose place we need to check michael@0: * @return "toolbar" if the node is in a toolbar, "panel" if it is in the michael@0: * menu panel, "palette" if it is in the (visible!) customization michael@0: * palette, undefined otherwise. michael@0: */ michael@0: getPlaceForItem: function(aElement) { michael@0: let place; michael@0: let node = aElement; michael@0: while (node && !place) { michael@0: if (node.localName == "toolbar") michael@0: place = "toolbar"; michael@0: else if (node.id == CustomizableUI.AREA_PANEL) michael@0: place = "panel"; michael@0: else if (node.id == "customization-palette") michael@0: place = "palette"; michael@0: michael@0: node = node.parentNode; michael@0: } michael@0: return place; michael@0: }, michael@0: michael@0: /** michael@0: * Check if a toolbar is builtin or not. michael@0: * @param aToolbarId the ID of the toolbar you want to check michael@0: */ michael@0: isBuiltinToolbar: function(aToolbarId) { michael@0: return CustomizableUIInternal._builtinToolbars.has(aToolbarId); michael@0: }, michael@0: }; michael@0: Object.freeze(this.CustomizableUI); michael@0: Object.freeze(this.CustomizableUI.windows); michael@0: michael@0: /** michael@0: * All external consumers of widgets are really interacting with these wrappers michael@0: * which provide a common interface. michael@0: */ michael@0: michael@0: /** michael@0: * WidgetGroupWrapper is the common interface for interacting with an entire michael@0: * widget group - AKA, all instances of a widget across a series of windows. michael@0: * This particular wrapper is only used for widgets created via the provider michael@0: * API. michael@0: */ michael@0: function WidgetGroupWrapper(aWidget) { michael@0: this.isGroup = true; michael@0: michael@0: const kBareProps = ["id", "source", "type", "disabled", "label", "tooltiptext", michael@0: "showInPrivateBrowsing"]; michael@0: for (let prop of kBareProps) { michael@0: let propertyName = prop; michael@0: this.__defineGetter__(propertyName, function() aWidget[propertyName]); michael@0: } michael@0: michael@0: this.__defineGetter__("provider", function() CustomizableUI.PROVIDER_API); michael@0: michael@0: this.__defineSetter__("disabled", function(aValue) { michael@0: aValue = !!aValue; michael@0: aWidget.disabled = aValue; michael@0: for (let [,instance] of aWidget.instances) { michael@0: instance.disabled = aValue; michael@0: } michael@0: }); michael@0: michael@0: this.forWindow = function WidgetGroupWrapper_forWindow(aWindow) { michael@0: let wrapperMap; michael@0: if (!gSingleWrapperCache.has(aWindow)) { michael@0: wrapperMap = new Map(); michael@0: gSingleWrapperCache.set(aWindow, wrapperMap); michael@0: } else { michael@0: wrapperMap = gSingleWrapperCache.get(aWindow); michael@0: } michael@0: if (wrapperMap.has(aWidget.id)) { michael@0: return wrapperMap.get(aWidget.id); michael@0: } michael@0: michael@0: let instance = aWidget.instances.get(aWindow.document); michael@0: if (!instance && michael@0: (aWidget.showInPrivateBrowsing || !PrivateBrowsingUtils.isWindowPrivate(aWindow))) { michael@0: instance = CustomizableUIInternal.buildWidget(aWindow.document, michael@0: aWidget); michael@0: } michael@0: michael@0: let wrapper = new WidgetSingleWrapper(aWidget, instance); michael@0: wrapperMap.set(aWidget.id, wrapper); michael@0: return wrapper; michael@0: }; michael@0: michael@0: this.__defineGetter__("instances", function() { michael@0: // Can't use gBuildWindows here because some areas load lazily: michael@0: let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id); michael@0: if (!placement) { michael@0: return []; michael@0: } michael@0: let area = placement.area; michael@0: let buildAreas = gBuildAreas.get(area); michael@0: if (!buildAreas) { michael@0: return []; michael@0: } michael@0: return [this.forWindow(node.ownerDocument.defaultView) for (node of buildAreas)]; michael@0: }); michael@0: michael@0: this.__defineGetter__("areaType", function() { michael@0: let areaProps = gAreas.get(aWidget.currentArea); michael@0: return areaProps && areaProps.get("type"); michael@0: }); michael@0: michael@0: Object.freeze(this); michael@0: } michael@0: michael@0: /** michael@0: * A WidgetSingleWrapper is a wrapper around a single instance of a widget in michael@0: * a particular window. michael@0: */ michael@0: function WidgetSingleWrapper(aWidget, aNode) { michael@0: this.isGroup = false; michael@0: michael@0: this.node = aNode; michael@0: this.provider = CustomizableUI.PROVIDER_API; michael@0: michael@0: const kGlobalProps = ["id", "type"]; michael@0: for (let prop of kGlobalProps) { michael@0: this[prop] = aWidget[prop]; michael@0: } michael@0: michael@0: const kNodeProps = ["label", "tooltiptext"]; michael@0: for (let prop of kNodeProps) { michael@0: let propertyName = prop; michael@0: // Look at the node for these, instead of the widget data, to ensure the michael@0: // wrapper always reflects this live instance. michael@0: this.__defineGetter__(propertyName, michael@0: function() aNode.getAttribute(propertyName)); michael@0: } michael@0: michael@0: this.__defineGetter__("disabled", function() aNode.disabled); michael@0: this.__defineSetter__("disabled", function(aValue) { michael@0: aNode.disabled = !!aValue; michael@0: }); michael@0: michael@0: this.__defineGetter__("anchor", function() { michael@0: let anchorId; michael@0: // First check for an anchor for the area: michael@0: let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id); michael@0: if (placement) { michael@0: anchorId = gAreas.get(placement.area).get("anchor"); michael@0: } michael@0: if (!anchorId) { michael@0: anchorId = aNode.getAttribute("cui-anchorid"); michael@0: } michael@0: michael@0: return anchorId ? aNode.ownerDocument.getElementById(anchorId) michael@0: : aNode; michael@0: }); michael@0: michael@0: this.__defineGetter__("overflowed", function() { michael@0: return aNode.getAttribute("overflowedItem") == "true"; michael@0: }); michael@0: michael@0: Object.freeze(this); michael@0: } michael@0: michael@0: /** michael@0: * XULWidgetGroupWrapper is the common interface for interacting with an entire michael@0: * widget group - AKA, all instances of a widget across a series of windows. michael@0: * This particular wrapper is only used for widgets created via the old-school michael@0: * XUL method (overlays, or programmatically injecting toolbaritems, or other michael@0: * such things). michael@0: */ michael@0: //XXXunf Going to need to hook this up to some events to keep it all live. michael@0: function XULWidgetGroupWrapper(aWidgetId) { michael@0: this.isGroup = true; michael@0: this.id = aWidgetId; michael@0: this.type = "custom"; michael@0: this.provider = CustomizableUI.PROVIDER_XUL; michael@0: michael@0: this.forWindow = function XULWidgetGroupWrapper_forWindow(aWindow) { michael@0: let wrapperMap; michael@0: if (!gSingleWrapperCache.has(aWindow)) { michael@0: wrapperMap = new Map(); michael@0: gSingleWrapperCache.set(aWindow, wrapperMap); michael@0: } else { michael@0: wrapperMap = gSingleWrapperCache.get(aWindow); michael@0: } michael@0: if (wrapperMap.has(aWidgetId)) { michael@0: return wrapperMap.get(aWidgetId); michael@0: } michael@0: michael@0: let instance = aWindow.document.getElementById(aWidgetId); michael@0: if (!instance) { michael@0: // Toolbar palettes aren't part of the document, so elements in there michael@0: // won't be found via document.getElementById(). michael@0: instance = aWindow.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0]; michael@0: } michael@0: michael@0: let wrapper = new XULWidgetSingleWrapper(aWidgetId, instance, aWindow.document); michael@0: wrapperMap.set(aWidgetId, wrapper); michael@0: return wrapper; michael@0: }; michael@0: michael@0: this.__defineGetter__("areaType", function() { michael@0: let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId); michael@0: if (!placement) { michael@0: return null; michael@0: } michael@0: michael@0: let areaProps = gAreas.get(placement.area); michael@0: return areaProps && areaProps.get("type"); michael@0: }); michael@0: michael@0: this.__defineGetter__("instances", function() { michael@0: return [this.forWindow(win) for ([win,] of gBuildWindows)]; michael@0: }); michael@0: michael@0: Object.freeze(this); michael@0: } michael@0: michael@0: /** michael@0: * A XULWidgetSingleWrapper is a wrapper around a single instance of a XUL michael@0: * widget in a particular window. michael@0: */ michael@0: function XULWidgetSingleWrapper(aWidgetId, aNode, aDocument) { michael@0: this.isGroup = false; michael@0: michael@0: this.id = aWidgetId; michael@0: this.type = "custom"; michael@0: this.provider = CustomizableUI.PROVIDER_XUL; michael@0: michael@0: let weakDoc = Cu.getWeakReference(aDocument); michael@0: // If we keep a strong ref, the weak ref will never die, so null it out: michael@0: aDocument = null; michael@0: michael@0: this.__defineGetter__("node", function() { michael@0: // If we've set this to null (further down), we're sure there's nothing to michael@0: // be gotten here, so bail out early: michael@0: if (!weakDoc) { michael@0: return null; michael@0: } michael@0: if (aNode) { michael@0: // Return the last known node if it's still in the DOM... michael@0: if (aNode.ownerDocument.contains(aNode)) { michael@0: return aNode; michael@0: } michael@0: // ... or the toolbox michael@0: let toolbox = aNode.ownerDocument.defaultView.gNavToolbox; michael@0: if (toolbox && toolbox.palette && aNode.parentNode == toolbox.palette) { michael@0: return aNode; michael@0: } michael@0: // If it isn't, clear the cached value and fall through to the "slow" case: michael@0: aNode = null; michael@0: } michael@0: michael@0: let doc = weakDoc.get(); michael@0: if (doc) { michael@0: // Store locally so we can cache the result: michael@0: aNode = CustomizableUIInternal.findWidgetInWindow(aWidgetId, doc.defaultView); michael@0: return aNode; michael@0: } michael@0: // The weakref to the document is dead, we're done here forever more: michael@0: weakDoc = null; michael@0: return null; michael@0: }); michael@0: michael@0: this.__defineGetter__("anchor", function() { michael@0: let anchorId; michael@0: // First check for an anchor for the area: michael@0: let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId); michael@0: if (placement) { michael@0: anchorId = gAreas.get(placement.area).get("anchor"); michael@0: } michael@0: michael@0: let node = this.node; michael@0: if (!anchorId && node) { michael@0: anchorId = node.getAttribute("cui-anchorid"); michael@0: } michael@0: michael@0: return (anchorId && node) ? node.ownerDocument.getElementById(anchorId) : node; michael@0: }); michael@0: michael@0: this.__defineGetter__("overflowed", function() { michael@0: let node = this.node; michael@0: if (!node) { michael@0: return false; michael@0: } michael@0: return node.getAttribute("overflowedItem") == "true"; michael@0: }); michael@0: michael@0: Object.freeze(this); michael@0: } michael@0: michael@0: const LAZY_RESIZE_INTERVAL_MS = 200; michael@0: michael@0: function OverflowableToolbar(aToolbarNode) { michael@0: this._toolbar = aToolbarNode; michael@0: this._collapsed = new Map(); michael@0: this._enabled = true; michael@0: michael@0: this._toolbar.setAttribute("overflowable", "true"); michael@0: let doc = this._toolbar.ownerDocument; michael@0: this._target = this._toolbar.customizationTarget; michael@0: this._list = doc.getElementById(this._toolbar.getAttribute("overflowtarget")); michael@0: this._list.toolbox = this._toolbar.toolbox; michael@0: this._list.customizationTarget = this._list; michael@0: michael@0: let window = this._toolbar.ownerDocument.defaultView; michael@0: if (window.gBrowserInit.delayedStartupFinished) { michael@0: this.init(); michael@0: } else { michael@0: Services.obs.addObserver(this, "browser-delayed-startup-finished", false); michael@0: } michael@0: } michael@0: michael@0: OverflowableToolbar.prototype = { michael@0: initialized: false, michael@0: _forceOnOverflow: false, michael@0: michael@0: observe: function(aSubject, aTopic, aData) { michael@0: if (aTopic == "browser-delayed-startup-finished" && michael@0: aSubject == this._toolbar.ownerDocument.defaultView) { michael@0: Services.obs.removeObserver(this, "browser-delayed-startup-finished"); michael@0: this.init(); michael@0: } michael@0: }, michael@0: michael@0: init: function() { michael@0: let doc = this._toolbar.ownerDocument; michael@0: let window = doc.defaultView; michael@0: window.addEventListener("resize", this); michael@0: window.gNavToolbox.addEventListener("customizationstarting", this); michael@0: window.gNavToolbox.addEventListener("aftercustomization", this); michael@0: michael@0: let chevronId = this._toolbar.getAttribute("overflowbutton"); michael@0: this._chevron = doc.getElementById(chevronId); michael@0: this._chevron.addEventListener("command", this); michael@0: michael@0: let panelId = this._toolbar.getAttribute("overflowpanel"); michael@0: this._panel = doc.getElementById(panelId); michael@0: this._panel.addEventListener("popuphiding", this); michael@0: CustomizableUIInternal.addPanelCloseListeners(this._panel); michael@0: michael@0: CustomizableUI.addListener(this); michael@0: michael@0: // The 'overflow' event may have been fired before init was called. michael@0: if (this._toolbar.overflowedDuringConstruction) { michael@0: this.onOverflow(this._toolbar.overflowedDuringConstruction); michael@0: this._toolbar.overflowedDuringConstruction = null; michael@0: } michael@0: michael@0: this.initialized = true; michael@0: }, michael@0: michael@0: uninit: function() { michael@0: this._toolbar.removeEventListener("overflow", this._toolbar); michael@0: this._toolbar.removeEventListener("underflow", this._toolbar); michael@0: this._toolbar.removeAttribute("overflowable"); michael@0: michael@0: if (!this.initialized) { michael@0: Services.obs.removeObserver(this, "browser-delayed-startup-finished"); michael@0: return; michael@0: } michael@0: michael@0: this._disable(); michael@0: michael@0: let window = this._toolbar.ownerDocument.defaultView; michael@0: window.removeEventListener("resize", this); michael@0: window.gNavToolbox.removeEventListener("customizationstarting", this); michael@0: window.gNavToolbox.removeEventListener("aftercustomization", this); michael@0: this._chevron.removeEventListener("command", this); michael@0: this._panel.removeEventListener("popuphiding", this); michael@0: CustomizableUI.removeListener(this); michael@0: CustomizableUIInternal.removePanelCloseListeners(this._panel); michael@0: }, michael@0: michael@0: handleEvent: function(aEvent) { michael@0: switch(aEvent.type) { michael@0: case "resize": michael@0: this._onResize(aEvent); michael@0: break; michael@0: case "command": michael@0: if (aEvent.target == this._chevron) { michael@0: this._onClickChevron(aEvent); michael@0: } else { michael@0: this._panel.hidePopup(); michael@0: } michael@0: break; michael@0: case "popuphiding": michael@0: this._onPanelHiding(aEvent); michael@0: break; michael@0: case "customizationstarting": michael@0: this._disable(); michael@0: break; michael@0: case "aftercustomization": michael@0: this._enable(); michael@0: break; michael@0: } michael@0: }, michael@0: michael@0: show: function() { michael@0: let deferred = Promise.defer(); michael@0: if (this._panel.state == "open") { michael@0: deferred.resolve(); michael@0: return deferred.promise; michael@0: } michael@0: let doc = this._panel.ownerDocument; michael@0: this._panel.hidden = false; michael@0: let contextMenu = doc.getElementById(this._panel.getAttribute("context")); michael@0: gELS.addSystemEventListener(contextMenu, 'command', this, true); michael@0: let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon"); michael@0: this._panel.openPopup(anchor || this._chevron); michael@0: this._chevron.open = true; michael@0: michael@0: this._panel.addEventListener("popupshown", function onPopupShown() { michael@0: this.removeEventListener("popupshown", onPopupShown); michael@0: deferred.resolve(); michael@0: }); michael@0: michael@0: return deferred.promise; michael@0: }, michael@0: michael@0: _onClickChevron: function(aEvent) { michael@0: if (this._chevron.open) { michael@0: this._panel.hidePopup(); michael@0: this._chevron.open = false; michael@0: } else { michael@0: this.show(); michael@0: } michael@0: }, michael@0: michael@0: _onPanelHiding: function(aEvent) { michael@0: this._chevron.open = false; michael@0: let doc = aEvent.target.ownerDocument; michael@0: let contextMenu = doc.getElementById(this._panel.getAttribute("context")); michael@0: gELS.removeSystemEventListener(contextMenu, 'command', this, true); michael@0: }, michael@0: michael@0: onOverflow: function(aEvent) { michael@0: if (!this._enabled || michael@0: (aEvent && aEvent.target != this._toolbar.customizationTarget)) michael@0: return; michael@0: michael@0: let child = this._target.lastChild; michael@0: michael@0: while (child && this._target.scrollLeftMax > 0) { michael@0: let prevChild = child.previousSibling; michael@0: michael@0: if (child.getAttribute("overflows") != "false") { michael@0: this._collapsed.set(child.id, this._target.clientWidth); michael@0: child.setAttribute("overflowedItem", true); michael@0: child.setAttribute("cui-anchorid", this._chevron.id); michael@0: CustomizableUIInternal.notifyListeners("onWidgetOverflow", child, this._target); michael@0: michael@0: this._list.insertBefore(child, this._list.firstChild); michael@0: if (!this._toolbar.hasAttribute("overflowing")) { michael@0: CustomizableUI.addListener(this); michael@0: } michael@0: this._toolbar.setAttribute("overflowing", "true"); michael@0: } michael@0: child = prevChild; michael@0: }; michael@0: michael@0: let win = this._target.ownerDocument.defaultView; michael@0: win.UpdateUrlbarSearchSplitterState(); michael@0: }, michael@0: michael@0: _onResize: function(aEvent) { michael@0: if (!this._lazyResizeHandler) { michael@0: this._lazyResizeHandler = new DeferredTask(this._onLazyResize.bind(this), michael@0: LAZY_RESIZE_INTERVAL_MS); michael@0: } michael@0: this._lazyResizeHandler.arm(); michael@0: }, michael@0: michael@0: _moveItemsBackToTheirOrigin: function(shouldMoveAllItems) { michael@0: let placements = gPlacements.get(this._toolbar.id); michael@0: while (this._list.firstChild) { michael@0: let child = this._list.firstChild; michael@0: let minSize = this._collapsed.get(child.id); michael@0: michael@0: if (!shouldMoveAllItems && michael@0: minSize && michael@0: this._target.clientWidth <= minSize) { michael@0: return; michael@0: } michael@0: michael@0: this._collapsed.delete(child.id); michael@0: let beforeNodeIndex = placements.indexOf(child.id) + 1; michael@0: // If this is a skipintoolbarset item, meaning it doesn't occur in the placements list, michael@0: // we're inserting it at the end. This will mean first-in, first-out (more or less) michael@0: // leading to as little change in order as possible. michael@0: if (beforeNodeIndex == 0) { michael@0: beforeNodeIndex = placements.length; michael@0: } michael@0: let inserted = false; michael@0: for (; beforeNodeIndex < placements.length; beforeNodeIndex++) { michael@0: let beforeNode = this._target.getElementsByAttribute("id", placements[beforeNodeIndex])[0]; michael@0: if (beforeNode) { michael@0: this._target.insertBefore(child, beforeNode); michael@0: inserted = true; michael@0: break; michael@0: } michael@0: } michael@0: if (!inserted) { michael@0: this._target.appendChild(child); michael@0: } michael@0: child.removeAttribute("cui-anchorid"); michael@0: child.removeAttribute("overflowedItem"); michael@0: CustomizableUIInternal.notifyListeners("onWidgetUnderflow", child, this._target); michael@0: } michael@0: michael@0: let win = this._target.ownerDocument.defaultView; michael@0: win.UpdateUrlbarSearchSplitterState(); michael@0: michael@0: if (!this._collapsed.size) { michael@0: this._toolbar.removeAttribute("overflowing"); michael@0: CustomizableUI.removeListener(this); michael@0: } michael@0: }, michael@0: michael@0: _onLazyResize: function() { michael@0: if (!this._enabled) michael@0: return; michael@0: michael@0: if (this._target.scrollLeftMax > 0) { michael@0: this.onOverflow(); michael@0: } else { michael@0: this._moveItemsBackToTheirOrigin(); michael@0: } michael@0: }, michael@0: michael@0: _disable: function() { michael@0: this._enabled = false; michael@0: this._moveItemsBackToTheirOrigin(true); michael@0: if (this._lazyResizeHandler) { michael@0: this._lazyResizeHandler.disarm(); michael@0: } michael@0: }, michael@0: michael@0: _enable: function() { michael@0: this._enabled = true; michael@0: this.onOverflow(); michael@0: }, michael@0: michael@0: onWidgetBeforeDOMChange: function(aNode, aNextNode, aContainer) { michael@0: if (aContainer != this._target && aContainer != this._list) { michael@0: return; michael@0: } michael@0: // When we (re)move an item, update all the items that come after it in the list michael@0: // with the minsize *of the item before the to-be-removed node*. This way, we michael@0: // ensure that we try to move items back as soon as that's possible. michael@0: if (aNode.parentNode == this._list) { michael@0: let updatedMinSize; michael@0: if (aNode.previousSibling) { michael@0: updatedMinSize = this._collapsed.get(aNode.previousSibling.id); michael@0: } else { michael@0: // Force (these) items to try to flow back into the bar: michael@0: updatedMinSize = 1; michael@0: } michael@0: let nextItem = aNode.nextSibling; michael@0: while (nextItem) { michael@0: this._collapsed.set(nextItem.id, updatedMinSize); michael@0: nextItem = nextItem.nextSibling; michael@0: } michael@0: } michael@0: }, michael@0: michael@0: onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer) { michael@0: if (aContainer != this._target && aContainer != this._list) { michael@0: return; michael@0: } michael@0: michael@0: let nowInBar = aNode.parentNode == aContainer; michael@0: let nowOverflowed = aNode.parentNode == this._list; michael@0: let wasOverflowed = this._collapsed.has(aNode.id); michael@0: michael@0: // If this wasn't overflowed before... michael@0: if (!wasOverflowed) { michael@0: // ... but it is now, then we added to the overflow panel. Exciting stuff: michael@0: if (nowOverflowed) { michael@0: // NB: we're guaranteed that it has a previousSibling, because if it didn't, michael@0: // we would have added it to the toolbar instead. See getOverflowedNextNode. michael@0: let prevId = aNode.previousSibling.id; michael@0: let minSize = this._collapsed.get(prevId); michael@0: this._collapsed.set(aNode.id, minSize); michael@0: aNode.setAttribute("cui-anchorid", this._chevron.id); michael@0: aNode.setAttribute("overflowedItem", true); michael@0: CustomizableUIInternal.notifyListeners("onWidgetOverflow", aNode, this._target); michael@0: } michael@0: // If it is not overflowed and not in the toolbar, and was not overflowed michael@0: // either, it moved out of the toolbar. That means there's now space in there! michael@0: // Let's try to move stuff back: michael@0: else if (!nowInBar) { michael@0: this._moveItemsBackToTheirOrigin(true); michael@0: } michael@0: // If it's in the toolbar now, then we don't care. An overflow event may michael@0: // fire afterwards; that's ok! michael@0: } michael@0: // If it used to be overflowed... michael@0: else { michael@0: // ... and isn't anymore, let's remove our bookkeeping: michael@0: if (!nowOverflowed) { michael@0: this._collapsed.delete(aNode.id); michael@0: aNode.removeAttribute("cui-anchorid"); michael@0: aNode.removeAttribute("overflowedItem"); michael@0: CustomizableUIInternal.notifyListeners("onWidgetUnderflow", aNode, this._target); michael@0: michael@0: if (!this._collapsed.size) { michael@0: this._toolbar.removeAttribute("overflowing"); michael@0: CustomizableUI.removeListener(this); michael@0: } michael@0: } michael@0: // but if it still is, it must have changed places. Bookkeep: michael@0: else { michael@0: if (aNode.previousSibling) { michael@0: let prevId = aNode.previousSibling.id; michael@0: let minSize = this._collapsed.get(prevId); michael@0: this._collapsed.set(aNode.id, minSize); michael@0: } else { michael@0: // If it's now the first item in the overflow list, michael@0: // maybe we can return it: michael@0: this._moveItemsBackToTheirOrigin(); michael@0: } michael@0: } michael@0: } michael@0: }, michael@0: michael@0: findOverflowedInsertionPoints: function(aNode) { michael@0: let newNodeCanOverflow = aNode.getAttribute("overflows") != "false"; michael@0: let areaId = this._toolbar.id; michael@0: let placements = gPlacements.get(areaId); michael@0: let nodeIndex = placements.indexOf(aNode.id); michael@0: let nodeBeforeNewNodeIsOverflown = false; michael@0: michael@0: let loopIndex = -1; michael@0: while (++loopIndex < placements.length) { michael@0: let nextNodeId = placements[loopIndex]; michael@0: if (loopIndex > nodeIndex) { michael@0: if (newNodeCanOverflow && this._collapsed.has(nextNodeId)) { michael@0: let nextNode = this._list.getElementsByAttribute("id", nextNodeId).item(0); michael@0: if (nextNode) { michael@0: return [this._list, nextNode]; michael@0: } michael@0: } michael@0: if (!nodeBeforeNewNodeIsOverflown || !newNodeCanOverflow) { michael@0: let nextNode = this._target.getElementsByAttribute("id", nextNodeId).item(0); michael@0: if (nextNode) { michael@0: return [this._target, nextNode]; michael@0: } michael@0: } michael@0: } else if (loopIndex < nodeIndex && this._collapsed.has(nextNodeId)) { michael@0: nodeBeforeNewNodeIsOverflown = true; michael@0: } michael@0: } michael@0: michael@0: let containerForAppending = (this._collapsed.size && newNodeCanOverflow) ? michael@0: this._list : this._target; michael@0: return [containerForAppending, null]; michael@0: }, michael@0: michael@0: getContainerFor: function(aNode) { michael@0: if (aNode.getAttribute("overflowedItem") == "true") { michael@0: return this._list; michael@0: } michael@0: return this._target; michael@0: }, michael@0: }; michael@0: michael@0: CustomizableUIInternal.initialize();