diff options
Diffstat (limited to 'browser/components/customizableui')
15 files changed, 0 insertions, 10796 deletions
diff --git a/browser/components/customizableui/CustomizableUI.jsm b/browser/components/customizableui/CustomizableUI.jsm deleted file mode 100644 index cb0f519b2..000000000 --- a/browser/components/customizableui/CustomizableUI.jsm +++ /dev/null @@ -1,4401 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["CustomizableUI"]; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/AppConstants.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PanelWideWidgetTracker", - "resource:///modules/PanelWideWidgetTracker.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "CustomizableWidgets", - "resource:///modules/CustomizableWidgets.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "DeferredTask", - "resource://gre/modules/DeferredTask.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", - "resource://gre/modules/PrivateBrowsingUtils.jsm"); -XPCOMUtils.defineLazyGetter(this, "gWidgetsBundle", function() { - const kUrl = "chrome://browser/locale/customizableui/customizableWidgets.properties"; - return Services.strings.createBundle(kUrl); -}); -XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils", - "resource://gre/modules/ShortcutUtils.jsm"); -XPCOMUtils.defineLazyServiceGetter(this, "gELS", - "@mozilla.org/eventlistenerservice;1", "nsIEventListenerService"); -XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager", - "resource://gre/modules/LightweightThemeManager.jsm"); - -const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; - -const kSpecialWidgetPfx = "customizableui-special-"; - -const kPrefCustomizationState = "browser.uiCustomization.state"; -const kPrefCustomizationAutoAdd = "browser.uiCustomization.autoAdd"; -const kPrefCustomizationDebug = "browser.uiCustomization.debug"; -const kPrefDrawInTitlebar = "browser.tabs.drawInTitlebar"; -const kPrefWebIDEInNavbar = "devtools.webide.widget.inNavbarByDefault"; - -const kExpectedWindowURL = "chrome://browser/content/browser.xul"; - -/** - * The keys are the handlers that are fired when the event type (the value) - * is fired on the subview. A widget that provides a subview has the option - * of providing onViewShowing and onViewHiding event handlers. - */ -const kSubviewEvents = [ - "ViewShowing", - "ViewHiding" -]; - -/** - * The current version. We can use this to auto-add new default widgets as necessary. - * (would be const but isn't because of testing purposes) - */ -var kVersion = 6; - -/** - * Buttons removed from built-ins by version they were removed. kVersion must be - * bumped any time a new id is added to this. Use the button id as key, and - * version the button is removed in as the value. e.g. "pocket-button": 5 - */ -var ObsoleteBuiltinButtons = { - "pocket-button": 6 -}; - -/** - * gPalette is a map of every widget that CustomizableUI.jsm knows about, keyed - * on their IDs. - */ -var gPalette = new Map(); - -/** - * gAreas maps area IDs to Sets of properties about those areas. An area is a - * place where a widget can be put. - */ -var gAreas = new Map(); - -/** - * gPlacements maps area IDs to Arrays of widget IDs, indicating that the widgets - * are placed within that area (either directly in the area node, or in the - * customizationTarget of the node). - */ -var gPlacements = new Map(); - -/** - * gFuturePlacements represent placements that will happen for areas that have - * not yet loaded (due to lazy-loading). This can occur when add-ons register - * widgets. - */ -var gFuturePlacements = new Map(); - -// XXXunf Temporary. Need a nice way to abstract functions to build widgets -// of these types. -var gSupportedWidgetTypes = new Set(["button", "view", "custom"]); - -/** - * gPanelsForWindow is a list of known panels in a window which we may need to close - * should command events fire which target them. - */ -var gPanelsForWindow = new WeakMap(); - -/** - * gSeenWidgets remembers which widgets the user has seen for the first time - * before. This way, if a new widget is created, and the user has not seen it - * before, it can be put in its default location. Otherwise, it remains in the - * palette. - */ -var gSeenWidgets = new Set(); - -/** - * gDirtyAreaCache is a set of area IDs for areas where items have been added, - * moved or removed at least once. This set is persisted, and is used to - * optimize building of toolbars in the default case where no toolbars should - * be "dirty". - */ -var gDirtyAreaCache = new Set(); - -/** - * gPendingBuildAreas is a map from area IDs to map from build nodes to their - * existing children at the time of node registration, that are waiting - * for the area to be registered - */ -var gPendingBuildAreas = new Map(); - -var gSavedState = null; -var gRestoring = false; -var gDirty = false; -var gInBatchStack = 0; -var gResetting = false; -var gUndoResetting = false; - -/** - * gBuildAreas maps area IDs to actual area nodes within browser windows. - */ -var gBuildAreas = new Map(); - -/** - * gBuildWindows is a map of windows that have registered build areas, mapped - * to a Set of known toolboxes in that window. - */ -var gBuildWindows = new Map(); - -var gNewElementCount = 0; -var gGroupWrapperCache = new Map(); -var gSingleWrapperCache = new WeakMap(); -var gListeners = new Set(); - -var gUIStateBeforeReset = { - uiCustomizationState: null, - drawInTitlebar: null, - currentTheme: null, -}; - -XPCOMUtils.defineLazyGetter(this, "log", () => { - let scope = {}; - Cu.import("resource://gre/modules/Console.jsm", scope); - let debug; - try { - debug = Services.prefs.getBoolPref(kPrefCustomizationDebug); - } catch (ex) {} - let consoleOptions = { - maxLogLevel: debug ? "all" : "log", - prefix: "CustomizableUI", - }; - return new scope.ConsoleAPI(consoleOptions); -}); - -var CustomizableUIInternal = { - initialize: function() { - log.debug("Initializing"); - - this.addListener(this); - this._defineBuiltInWidgets(); - this.loadSavedState(); - this._introduceNewBuiltinWidgets(); - this._markObsoleteBuiltinButtonsSeen(); - - /** - * Please be advised that adding items to the panel by default could - * cause CART talos test regressions. This might happen when the - * number of items in the panel causes the area to become "scrollable" - * during the last phases of the transition. See bug 1230671 for an - * example of this. Be sure that what you're adding really needs to go - * into the panel by default, and if it does, consider swapping - * something out for it. - */ - let panelPlacements = [ - "edit-controls", - "zoom-controls", - "new-window-button", - "privatebrowsing-button", - "save-page-button", - "print-button", - "history-panelmenu", - "fullscreen-button", - "find-button", - "preferences-button", - "add-ons-button", - "sync-button", - ]; - - if (!AppConstants.MOZ_DEV_EDITION) { - panelPlacements.splice(-1, 0, "developer-button"); - } - - let showCharacterEncoding = Services.prefs.getComplexValue( - "browser.menu.showCharacterEncoding", - Ci.nsIPrefLocalizedString - ).data; - if (showCharacterEncoding == "true") { - panelPlacements.push("characterencoding-button"); - } - - this.registerArea(CustomizableUI.AREA_PANEL, { - anchor: "PanelUI-menu-button", - type: CustomizableUI.TYPE_MENU_PANEL, - defaultPlacements: panelPlacements - }, true); - PanelWideWidgetTracker.init(); - - let navbarPlacements = [ - "urlbar-container", - "search-container", - "bookmarks-menu-button", - "downloads-button", - "home-button", - ]; - - if (AppConstants.MOZ_DEV_EDITION) { - navbarPlacements.splice(2, 0, "developer-button"); - } - - if (Services.prefs.getBoolPref(kPrefWebIDEInNavbar)) { - navbarPlacements.push("webide-button"); - } - - // Place this last, when createWidget is called for pocket, it will - // append to the toolbar. - if (Services.prefs.getPrefType("extensions.pocket.enabled") != Services.prefs.PREF_INVALID && - Services.prefs.getBoolPref("extensions.pocket.enabled")) { - navbarPlacements.push("pocket-button"); - } - - this.registerArea(CustomizableUI.AREA_NAVBAR, { - legacy: true, - type: CustomizableUI.TYPE_TOOLBAR, - overflowable: true, - defaultPlacements: navbarPlacements, - defaultCollapsed: false, - }, true); - - if (AppConstants.MENUBAR_CAN_AUTOHIDE) { - this.registerArea(CustomizableUI.AREA_MENUBAR, { - legacy: true, - type: CustomizableUI.TYPE_TOOLBAR, - defaultPlacements: [ - "menubar-items", - ], - defaultCollapsed: true, - }, true); - } - - this.registerArea(CustomizableUI.AREA_TABSTRIP, { - legacy: true, - type: CustomizableUI.TYPE_TOOLBAR, - defaultPlacements: [ - "tabbrowser-tabs", - "new-tab-button", - "alltabs-button", - ], - defaultCollapsed: null, - }, true); - this.registerArea(CustomizableUI.AREA_BOOKMARKS, { - legacy: true, - type: CustomizableUI.TYPE_TOOLBAR, - defaultPlacements: [ - "personal-bookmarks", - ], - defaultCollapsed: true, - }, true); - - this.registerArea(CustomizableUI.AREA_ADDONBAR, { - type: CustomizableUI.TYPE_TOOLBAR, - legacy: true, - defaultPlacements: ["addonbar-closebutton", "status-bar"], - defaultCollapsed: false, - }, true); - }, - - get _builtinToolbars() { - let toolbars = new Set([ - CustomizableUI.AREA_NAVBAR, - CustomizableUI.AREA_BOOKMARKS, - CustomizableUI.AREA_TABSTRIP, - CustomizableUI.AREA_ADDONBAR, - ]); - if (AppConstants.platform != "macosx") { - toolbars.add(CustomizableUI.AREA_MENUBAR); - } - return toolbars; - }, - - _defineBuiltInWidgets: function() { - for (let widgetDefinition of CustomizableWidgets) { - this.createBuiltinWidget(widgetDefinition); - } - }, - - _introduceNewBuiltinWidgets: function() { - // We should still enter even if gSavedState.currentVersion >= kVersion - // because the per-widget pref facility is independent of versioning. - if (!gSavedState) { - // Flip all the prefs so we don't try to re-introduce later: - for (let [, widget] of gPalette) { - if (widget.defaultArea && widget._introducedInVersion === "pref") { - let prefId = "browser.toolbarbuttons.introduced." + widget.id; - Services.prefs.setBoolPref(prefId, true); - } - } - return; - } - - let currentVersion = gSavedState.currentVersion; - for (let [id, widget] of gPalette) { - if (widget.defaultArea) { - let shouldAdd = false; - let shouldSetPref = false; - let prefId = "browser.toolbarbuttons.introduced." + widget.id; - if (widget._introducedInVersion === "pref") { - try { - shouldAdd = !Services.prefs.getBoolPref(prefId); - } catch (ex) { - // Pref doesn't exist: - shouldAdd = true; - } - shouldSetPref = shouldAdd; - } else if (widget._introducedInVersion > currentVersion) { - shouldAdd = true; - } - - if (shouldAdd) { - let futurePlacements = gFuturePlacements.get(widget.defaultArea); - if (futurePlacements) { - futurePlacements.add(id); - } else { - gFuturePlacements.set(widget.defaultArea, new Set([id])); - } - if (shouldSetPref) { - Services.prefs.setBoolPref(prefId, true); - } - } - } - } - - if (currentVersion < 2) { - // Nuke the old 'loop-call-button' out of orbit. - CustomizableUI.removeWidgetFromArea("loop-call-button"); - } - - if (currentVersion < 4) { - CustomizableUI.removeWidgetFromArea("loop-button-throttled"); - } - }, - - /** - * _markObsoleteBuiltinButtonsSeen - * when upgrading, ensure obsoleted buttons are in seen state. - */ - _markObsoleteBuiltinButtonsSeen: function() { - if (!gSavedState) - return; - let currentVersion = gSavedState.currentVersion; - if (currentVersion >= kVersion) - return; - // we're upgrading, update state if necessary - for (let id in ObsoleteBuiltinButtons) { - let version = ObsoleteBuiltinButtons[id] - if (version == kVersion) { - gSeenWidgets.add(id); - gDirty = true; - } - } - }, - - _placeNewDefaultWidgetsInArea: function(aArea) { - let futurePlacedWidgets = gFuturePlacements.get(aArea); - let savedPlacements = gSavedState && gSavedState.placements && gSavedState.placements[aArea]; - let defaultPlacements = gAreas.get(aArea).get("defaultPlacements"); - if (!savedPlacements || !savedPlacements.length || !futurePlacedWidgets || !defaultPlacements || - !defaultPlacements.length) { - return; - } - let defaultWidgetIndex = -1; - - for (let widgetId of futurePlacedWidgets) { - let widget = gPalette.get(widgetId); - if (!widget || widget.source !== CustomizableUI.SOURCE_BUILTIN || - !widget.defaultArea || !widget._introducedInVersion || - savedPlacements.indexOf(widget.id) !== -1) { - continue; - } - defaultWidgetIndex = defaultPlacements.indexOf(widget.id); - if (defaultWidgetIndex === -1) { - continue; - } - // Now we know that this widget should be here by default, was newly introduced, - // and we have a saved state to insert into, and a default state to work off of. - // Try introducing after widgets that come before it in the default placements: - for (let i = defaultWidgetIndex; i >= 0; i--) { - // Special case: if the defaults list this widget as coming first, insert at the beginning: - if (i === 0 && i === defaultWidgetIndex) { - savedPlacements.splice(0, 0, widget.id); - // Before you ask, yes, deleting things inside a let x of y loop where y is a Set is - // safe, and we won't skip any items. - futurePlacedWidgets.delete(widget.id); - gDirty = true; - break; - } - // Otherwise, if we're somewhere other than the beginning, check if the previous - // widget is in the saved placements. - if (i) { - let previousWidget = defaultPlacements[i - 1]; - let previousWidgetIndex = savedPlacements.indexOf(previousWidget); - if (previousWidgetIndex != -1) { - savedPlacements.splice(previousWidgetIndex + 1, 0, widget.id); - futurePlacedWidgets.delete(widget.id); - gDirty = true; - break; - } - } - } - // The loop above either inserts the item or doesn't - either way, we can get away - // with doing nothing else now; if the item remains in gFuturePlacements, we'll - // add it at the end in restoreStateForArea. - } - this.saveState(); - }, - - wrapWidget: function(aWidgetId) { - if (gGroupWrapperCache.has(aWidgetId)) { - return gGroupWrapperCache.get(aWidgetId); - } - - let provider = this.getWidgetProvider(aWidgetId); - if (!provider) { - return null; - } - - if (provider == CustomizableUI.PROVIDER_API) { - let widget = gPalette.get(aWidgetId); - if (!widget.wrapper) { - widget.wrapper = new WidgetGroupWrapper(widget); - gGroupWrapperCache.set(aWidgetId, widget.wrapper); - } - return widget.wrapper; - } - - // PROVIDER_SPECIAL gets treated the same as PROVIDER_XUL. - let wrapper = new XULWidgetGroupWrapper(aWidgetId); - gGroupWrapperCache.set(aWidgetId, wrapper); - return wrapper; - }, - - registerArea: function(aName, aProperties, aInternalCaller) { - if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) { - throw new Error("Invalid area name"); - } - - let areaIsKnown = gAreas.has(aName); - let props = areaIsKnown ? gAreas.get(aName) : new Map(); - const kImmutableProperties = new Set(["type", "legacy", "overflowable"]); - for (let key in aProperties) { - if (areaIsKnown && kImmutableProperties.has(key) && - props.get(key) != aProperties[key]) { - throw new Error("An area cannot change the property for '" + key + "'"); - } - // XXXgijs for special items, we need to make sure they have an appropriate ID - // so we aren't perpetually in a non-default state: - if (key == "defaultPlacements" && Array.isArray(aProperties[key])) { - props.set(key, aProperties[key].map(x => this.isSpecialWidget(x) ? this.ensureSpecialWidgetId(x) : x )); - } else { - props.set(key, aProperties[key]); - } - } - // Default to a toolbar: - if (!props.has("type")) { - props.set("type", CustomizableUI.TYPE_TOOLBAR); - } - if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { - // Check aProperties instead of props because this check is only interested - // in the passed arguments, not the state of a potentially pre-existing area. - if (!aInternalCaller && aProperties["defaultCollapsed"]) { - throw new Error("defaultCollapsed is only allowed for default toolbars.") - } - if (!props.has("defaultCollapsed")) { - props.set("defaultCollapsed", true); - } - } else if (props.has("defaultCollapsed")) { - throw new Error("defaultCollapsed only applies for TYPE_TOOLBAR areas."); - } - // Sanity check type: - let allTypes = [CustomizableUI.TYPE_TOOLBAR, CustomizableUI.TYPE_MENU_PANEL]; - if (allTypes.indexOf(props.get("type")) == -1) { - throw new Error("Invalid area type " + props.get("type")); - } - - // And to no placements: - if (!props.has("defaultPlacements")) { - props.set("defaultPlacements", []); - } - // Sanity check default placements array: - if (!Array.isArray(props.get("defaultPlacements"))) { - throw new Error("Should provide an array of default placements"); - } - - if (!areaIsKnown) { - gAreas.set(aName, props); - - // Reconcile new default widgets. Have to do this before we start restoring things. - this._placeNewDefaultWidgetsInArea(aName); - - if (props.get("legacy") && !gPlacements.has(aName)) { - // Guarantee this area exists in gFuturePlacements, to avoid checking it in - // various places elsewhere. - if (!gFuturePlacements.has(aName)) { - gFuturePlacements.set(aName, new Set()); - } - } else { - this.restoreStateForArea(aName); - } - - // If we have pending build area nodes, register all of them - if (gPendingBuildAreas.has(aName)) { - let pendingNodes = gPendingBuildAreas.get(aName); - for (let [pendingNode, existingChildren] of pendingNodes) { - this.registerToolbarNode(pendingNode, existingChildren); - } - gPendingBuildAreas.delete(aName); - } - } - }, - - unregisterArea: function(aName, aDestroyPlacements) { - if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) { - throw new Error("Invalid area name"); - } - if (!gAreas.has(aName) && !gPlacements.has(aName)) { - throw new Error("Area not registered"); - } - - // Move all the widgets out - this.beginBatchUpdate(); - try { - let placements = gPlacements.get(aName); - if (placements) { - // Need to clone this array so removeWidgetFromArea doesn't modify it - placements = [...placements]; - placements.forEach(this.removeWidgetFromArea, this); - } - - // Delete all remaining traces. - gAreas.delete(aName); - // Only destroy placements when necessary: - if (aDestroyPlacements) { - gPlacements.delete(aName); - } else { - // Otherwise we need to re-set them, as removeFromArea will have emptied - // them out: - gPlacements.set(aName, placements); - } - gFuturePlacements.delete(aName); - let existingAreaNodes = gBuildAreas.get(aName); - if (existingAreaNodes) { - for (let areaNode of existingAreaNodes) { - this.notifyListeners("onAreaNodeUnregistered", aName, areaNode.customizationTarget, - CustomizableUI.REASON_AREA_UNREGISTERED); - } - } - gBuildAreas.delete(aName); - } finally { - this.endBatchUpdate(true); - } - }, - - registerToolbarNode: function(aToolbar, aExistingChildren) { - let area = aToolbar.id; - if (gBuildAreas.has(area) && gBuildAreas.get(area).has(aToolbar)) { - return; - } - let areaProperties = gAreas.get(area); - - // If this area is not registered, try to do it automatically: - if (!areaProperties) { - // If there's no defaultset attribute and this isn't a legacy extra toolbar, - // we assume that we should wait for registerArea to be called: - if (!aToolbar.hasAttribute("defaultset") && - !aToolbar.hasAttribute("customindex")) { - if (!gPendingBuildAreas.has(area)) { - gPendingBuildAreas.set(area, new Map()); - } - let pendingNodes = gPendingBuildAreas.get(area); - pendingNodes.set(aToolbar, aExistingChildren); - return; - } - let props = {type: CustomizableUI.TYPE_TOOLBAR, legacy: true}; - let defaultsetAttribute = aToolbar.getAttribute("defaultset") || ""; - props.defaultPlacements = defaultsetAttribute.split(',').filter(s => s); - this.registerArea(area, props); - areaProperties = gAreas.get(area); - } - - this.beginBatchUpdate(); - try { - let placements = gPlacements.get(area); - if (!placements && areaProperties.has("legacy")) { - let legacyState = aToolbar.getAttribute("currentset"); - if (legacyState) { - legacyState = legacyState.split(",").filter(s => s); - } - - // Manually restore the state here, so the legacy state can be converted. - this.restoreStateForArea(area, legacyState); - placements = gPlacements.get(area); - } - - // Check that the current children and the current placements match. If - // not, mark it as dirty: - if (aExistingChildren.length != placements.length || - aExistingChildren.every((id, i) => id == placements[i])) { - gDirtyAreaCache.add(area); - } - - if (areaProperties.has("overflowable")) { - aToolbar.overflowable = new OverflowableToolbar(aToolbar); - } - - this.registerBuildArea(area, aToolbar); - - // We only build the toolbar if it's been marked as "dirty". Dirty means - // one of the following things: - // 1) Items have been added, moved or removed from this toolbar before. - // 2) The number of children of the toolbar does not match the length of - // the placements array for that area. - // - // This notion of being "dirty" is stored in a cache which is persisted - // in the saved state. - if (gDirtyAreaCache.has(area)) { - this.buildArea(area, placements, aToolbar); - } - this.notifyListeners("onAreaNodeRegistered", area, aToolbar.customizationTarget); - aToolbar.setAttribute("currentset", placements.join(",")); - } finally { - this.endBatchUpdate(); - } - }, - - buildArea: function(aArea, aPlacements, aAreaNode) { - let document = aAreaNode.ownerDocument; - let window = document.defaultView; - let inPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(window); - let container = aAreaNode.customizationTarget; - let areaIsPanel = gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL; - - if (!container) { - throw new Error("Expected area " + aArea - + " to have a customizationTarget attribute."); - } - - // Restore nav-bar visibility since it may have been hidden - // through a migration path (bug 938980) or an add-on. - if (aArea == CustomizableUI.AREA_NAVBAR) { - aAreaNode.collapsed = false; - } - - this.beginBatchUpdate(); - - try { - let currentNode = container.firstChild; - let placementsToRemove = new Set(); - for (let id of aPlacements) { - while (currentNode && currentNode.getAttribute("skipintoolbarset") == "true") { - currentNode = currentNode.nextSibling; - } - - if (currentNode && currentNode.id == id) { - currentNode = currentNode.nextSibling; - continue; - } - - if (this.isSpecialWidget(id) && areaIsPanel) { - placementsToRemove.add(id); - continue; - } - - let [provider, node] = this.getWidgetNode(id, window); - if (!node) { - log.debug("Unknown widget: " + id); - continue; - } - - let widget = null; - // If the placements have items in them which are (now) no longer removable, - // we shouldn't be moving them: - if (provider == CustomizableUI.PROVIDER_API) { - widget = gPalette.get(id); - if (!widget.removable && aArea != widget.defaultArea) { - placementsToRemove.add(id); - continue; - } - } else if (provider == CustomizableUI.PROVIDER_XUL && - node.parentNode != container && !this.isWidgetRemovable(node)) { - placementsToRemove.add(id); - continue; - } // Special widgets are always removable, so no need to check them - - if (inPrivateWindow && widget && !widget.showInPrivateBrowsing) { - continue; - } - - this.ensureButtonContextMenu(node, aAreaNode); - if (node.localName == "toolbarbutton") { - if (areaIsPanel) { - node.setAttribute("wrap", "true"); - } else { - node.removeAttribute("wrap"); - } - } - - // This needs updating in case we're resetting / undoing a reset. - if (widget) { - widget.currentArea = aArea; - } - this.insertWidgetBefore(node, currentNode, container, aArea); - if (gResetting) { - this.notifyListeners("onWidgetReset", node, container); - } else if (gUndoResetting) { - this.notifyListeners("onWidgetUndoMove", node, container); - } - } - - if (currentNode) { - let palette = aAreaNode.toolbox ? aAreaNode.toolbox.palette : null; - let limit = currentNode.previousSibling; - let node = container.lastChild; - while (node && node != limit) { - let previousSibling = node.previousSibling; - // Nodes opt-in to removability. If they're removable, and we haven't - // seen them in the placements array, then we toss them into the palette - // if one exists. If no palette exists, we just remove the node. If the - // node is not removable, we leave it where it is. However, we can only - // safely touch elements that have an ID - both because we depend on - // IDs, and because such elements are not intended to be widgets - // (eg, titlebar-placeholder elements). - if (node.id && node.getAttribute("skipintoolbarset") != "true") { - if (this.isWidgetRemovable(node)) { - if (palette && !this.isSpecialWidget(node.id)) { - palette.appendChild(node); - this.removeLocationAttributes(node); - } else { - container.removeChild(node); - } - } else { - node.setAttribute("removable", false); - log.debug("Adding non-removable widget to placements of " + aArea + ": " + - node.id); - gPlacements.get(aArea).push(node.id); - gDirty = true; - } - } - node = previousSibling; - } - } - - // If there are placements in here which aren't removable from their original area, - // we remove them from this area's placement array. They will (have) be(en) added - // to their original area's placements array in the block above this one. - if (placementsToRemove.size) { - let placementAry = gPlacements.get(aArea); - for (let id of placementsToRemove) { - let index = placementAry.indexOf(id); - placementAry.splice(index, 1); - } - } - - if (gResetting) { - this.notifyListeners("onAreaReset", aArea, container); - } - } finally { - this.endBatchUpdate(); - } - }, - - addPanelCloseListeners: function(aPanel) { - gELS.addSystemEventListener(aPanel, "click", this, false); - gELS.addSystemEventListener(aPanel, "keypress", this, false); - let win = aPanel.ownerGlobal; - if (!gPanelsForWindow.has(win)) { - gPanelsForWindow.set(win, new Set()); - } - gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel)); - }, - - removePanelCloseListeners: function(aPanel) { - gELS.removeSystemEventListener(aPanel, "click", this, false); - gELS.removeSystemEventListener(aPanel, "keypress", this, false); - let win = aPanel.ownerGlobal; - let panels = gPanelsForWindow.get(win); - if (panels) { - panels.delete(this._getPanelForNode(aPanel)); - } - }, - - ensureButtonContextMenu: function(aNode, aAreaNode) { - const kPanelItemContextMenu = "customizationPanelItemContextMenu"; - - let currentContextMenu = aNode.getAttribute("context") || - aNode.getAttribute("contextmenu"); - let place = CustomizableUI.getPlaceForItem(aAreaNode); - let contextMenuForPlace = place == "panel" ? - kPanelItemContextMenu : - null; - if (contextMenuForPlace && !currentContextMenu) { - aNode.setAttribute("context", contextMenuForPlace); - } else if (currentContextMenu == kPanelItemContextMenu && - contextMenuForPlace != kPanelItemContextMenu) { - aNode.removeAttribute("context"); - aNode.removeAttribute("contextmenu"); - } - }, - - getWidgetProvider: function(aWidgetId) { - if (this.isSpecialWidget(aWidgetId)) { - return CustomizableUI.PROVIDER_SPECIAL; - } - if (gPalette.has(aWidgetId)) { - return CustomizableUI.PROVIDER_API; - } - // If this was an API widget that was destroyed, return null: - if (gSeenWidgets.has(aWidgetId)) { - return null; - } - - // We fall back to the XUL provider, but we don't know for sure (at this - // point) whether it exists there either. So the API is technically lying. - // Ideally, it would be able to return an error value (or throw an - // exception) if it really didn't exist. Our code calling this function - // handles that fine, but this is a public API. - return CustomizableUI.PROVIDER_XUL; - }, - - getWidgetNode: function(aWidgetId, aWindow) { - let document = aWindow.document; - - if (this.isSpecialWidget(aWidgetId)) { - let widgetNode = document.getElementById(aWidgetId) || - this.createSpecialWidget(aWidgetId, document); - return [ CustomizableUI.PROVIDER_SPECIAL, widgetNode]; - } - - let widget = gPalette.get(aWidgetId); - if (widget) { - // If we have an instance of this widget already, just use that. - if (widget.instances.has(document)) { - log.debug("An instance of widget " + aWidgetId + " already exists in this " - + "document. Reusing."); - return [ CustomizableUI.PROVIDER_API, - widget.instances.get(document) ]; - } - - return [ CustomizableUI.PROVIDER_API, - this.buildWidget(document, widget) ]; - } - - log.debug("Searching for " + aWidgetId + " in toolbox."); - let node = this.findWidgetInWindow(aWidgetId, aWindow); - if (node) { - return [ CustomizableUI.PROVIDER_XUL, node ]; - } - - log.debug("No node for " + aWidgetId + " found."); - return [null, null]; - }, - - registerMenuPanel: function(aPanelContents) { - if (gBuildAreas.has(CustomizableUI.AREA_PANEL) && - gBuildAreas.get(CustomizableUI.AREA_PANEL).has(aPanelContents)) { - return; - } - - let document = aPanelContents.ownerDocument; - - aPanelContents.toolbox = document.getElementById("navigator-toolbox"); - aPanelContents.customizationTarget = aPanelContents; - - this.addPanelCloseListeners(this._getPanelForNode(aPanelContents)); - - let placements = gPlacements.get(CustomizableUI.AREA_PANEL); - this.buildArea(CustomizableUI.AREA_PANEL, placements, aPanelContents); - this.notifyListeners("onAreaNodeRegistered", CustomizableUI.AREA_PANEL, aPanelContents); - - for (let child of aPanelContents.children) { - if (child.localName != "toolbarbutton") { - if (child.localName == "toolbaritem") { - this.ensureButtonContextMenu(child, aPanelContents); - } - continue; - } - this.ensureButtonContextMenu(child, aPanelContents); - child.setAttribute("wrap", "true"); - } - - this.registerBuildArea(CustomizableUI.AREA_PANEL, aPanelContents); - }, - - onWidgetAdded: function(aWidgetId, aArea, aPosition) { - this.insertNode(aWidgetId, aArea, aPosition, true); - - if (!gResetting) { - this._clearPreviousUIState(); - } - }, - - onWidgetRemoved: function(aWidgetId, aArea) { - let areaNodes = gBuildAreas.get(aArea); - if (!areaNodes) { - return; - } - - let area = gAreas.get(aArea); - let isToolbar = area.get("type") == CustomizableUI.TYPE_TOOLBAR; - let isOverflowable = isToolbar && area.get("overflowable"); - let showInPrivateBrowsing = gPalette.has(aWidgetId) - ? gPalette.get(aWidgetId).showInPrivateBrowsing - : true; - - for (let areaNode of areaNodes) { - let window = areaNode.ownerGlobal; - if (!showInPrivateBrowsing && - PrivateBrowsingUtils.isWindowPrivate(window)) { - continue; - } - - let container = areaNode.customizationTarget; - let widgetNode = window.document.getElementById(aWidgetId); - if (widgetNode && isOverflowable) { - container = areaNode.overflowable.getContainerFor(widgetNode); - } - - if (!widgetNode || !container.contains(widgetNode)) { - log.info("Widget " + aWidgetId + " not found, unable to remove from " + aArea); - continue; - } - - this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null, container, true); - - // We remove location attributes here to make sure they're gone too when a - // widget is removed from a toolbar to the palette. See bug 930950. - this.removeLocationAttributes(widgetNode); - // We also need to remove the panel context menu if it's there: - this.ensureButtonContextMenu(widgetNode); - widgetNode.removeAttribute("wrap"); - if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) { - container.removeChild(widgetNode); - } else { - areaNode.toolbox.palette.appendChild(widgetNode); - } - this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null, container, true); - - if (isToolbar) { - areaNode.setAttribute("currentset", gPlacements.get(aArea).join(',')); - } - - let windowCache = gSingleWrapperCache.get(window); - if (windowCache) { - windowCache.delete(aWidgetId); - } - } - if (!gResetting) { - this._clearPreviousUIState(); - } - }, - - onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) { - this.insertNode(aWidgetId, aArea, aNewPosition); - if (!gResetting) { - this._clearPreviousUIState(); - } - }, - - onCustomizeEnd: function(aWindow) { - this._clearPreviousUIState(); - }, - - registerBuildArea: function(aArea, aNode) { - // We ensure that the window is registered to have its customization data - // cleaned up when unloading. - let window = aNode.ownerGlobal; - if (window.closed) { - return; - } - this.registerBuildWindow(window); - - // Also register this build area's toolbox. - if (aNode.toolbox) { - gBuildWindows.get(window).add(aNode.toolbox); - } - - if (!gBuildAreas.has(aArea)) { - gBuildAreas.set(aArea, new Set()); - } - - gBuildAreas.get(aArea).add(aNode); - - // Give a class to all customize targets to be used for styling in Customize Mode - let customizableNode = this.getCustomizeTargetForArea(aArea, window); - customizableNode.classList.add("customization-target"); - }, - - registerBuildWindow: function(aWindow) { - if (!gBuildWindows.has(aWindow)) { - gBuildWindows.set(aWindow, new Set()); - - aWindow.addEventListener("unload", this); - aWindow.addEventListener("command", this, true); - - this.notifyListeners("onWindowOpened", aWindow); - } - }, - - unregisterBuildWindow: function(aWindow) { - aWindow.removeEventListener("unload", this); - aWindow.removeEventListener("command", this, true); - gPanelsForWindow.delete(aWindow); - gBuildWindows.delete(aWindow); - gSingleWrapperCache.delete(aWindow); - let document = aWindow.document; - - for (let [areaId, areaNodes] of gBuildAreas) { - let areaProperties = gAreas.get(areaId); - for (let node of areaNodes) { - if (node.ownerDocument == document) { - this.notifyListeners("onAreaNodeUnregistered", areaId, node.customizationTarget, - CustomizableUI.REASON_WINDOW_CLOSED); - if (areaProperties.has("overflowable")) { - node.overflowable.uninit(); - node.overflowable = null; - } - areaNodes.delete(node); - } - } - } - - for (let [, widget] of gPalette) { - widget.instances.delete(document); - this.notifyListeners("onWidgetInstanceRemoved", widget.id, document); - } - - for (let [, areaMap] of gPendingBuildAreas) { - let toDelete = []; - for (let [areaNode, ] of areaMap) { - if (areaNode.ownerDocument == document) { - toDelete.push(areaNode); - } - } - for (let areaNode of toDelete) { - areaMap.delete(areaNode); - } - } - - this.notifyListeners("onWindowClosed", aWindow); - }, - - setLocationAttributes: function(aNode, aArea) { - let props = gAreas.get(aArea); - if (!props) { - throw new Error("Expected area " + aArea + " to have a properties Map " + - "associated with it."); - } - - aNode.setAttribute("cui-areatype", props.get("type") || ""); - let anchor = props.get("anchor"); - if (anchor) { - aNode.setAttribute("cui-anchorid", anchor); - } else { - aNode.removeAttribute("cui-anchorid"); - } - }, - - removeLocationAttributes: function(aNode) { - aNode.removeAttribute("cui-areatype"); - aNode.removeAttribute("cui-anchorid"); - }, - - insertNode: function(aWidgetId, aArea, aPosition, isNew) { - let areaNodes = gBuildAreas.get(aArea); - if (!areaNodes) { - return; - } - - let placements = gPlacements.get(aArea); - if (!placements) { - log.error("Could not find any placements for " + aArea + - " when moving a widget."); - return; - } - - // Go through each of the nodes associated with this area and move the - // widget to the requested location. - for (let areaNode of areaNodes) { - this.insertNodeInWindow(aWidgetId, areaNode, isNew); - } - }, - - insertNodeInWindow: function(aWidgetId, aAreaNode, isNew) { - let window = aAreaNode.ownerGlobal; - let showInPrivateBrowsing = gPalette.has(aWidgetId) - ? gPalette.get(aWidgetId).showInPrivateBrowsing - : true; - - if (!showInPrivateBrowsing && PrivateBrowsingUtils.isWindowPrivate(window)) { - return; - } - - let [, widgetNode] = this.getWidgetNode(aWidgetId, window); - if (!widgetNode) { - log.error("Widget '" + aWidgetId + "' not found, unable to move"); - return; - } - - let areaId = aAreaNode.id; - if (isNew) { - this.ensureButtonContextMenu(widgetNode, aAreaNode); - if (widgetNode.localName == "toolbarbutton" && areaId == CustomizableUI.AREA_PANEL) { - widgetNode.setAttribute("wrap", "true"); - } - } - - let [insertionContainer, nextNode] = this.findInsertionPoints(widgetNode, aAreaNode); - this.insertWidgetBefore(widgetNode, nextNode, insertionContainer, areaId); - - if (gAreas.get(areaId).get("type") == CustomizableUI.TYPE_TOOLBAR) { - aAreaNode.setAttribute("currentset", gPlacements.get(areaId).join(',')); - } - }, - - findInsertionPoints: function(aNode, aAreaNode) { - let areaId = aAreaNode.id; - let props = gAreas.get(areaId); - - // For overflowable toolbars, rely on them (because the work is more complicated): - if (props.get("type") == CustomizableUI.TYPE_TOOLBAR && props.get("overflowable")) { - return aAreaNode.overflowable.findOverflowedInsertionPoints(aNode); - } - - let container = aAreaNode.customizationTarget; - let placements = gPlacements.get(areaId); - let nodeIndex = placements.indexOf(aNode.id); - - while (++nodeIndex < placements.length) { - let nextNodeId = placements[nodeIndex]; - let nextNode = container.getElementsByAttribute("id", nextNodeId).item(0); - - if (nextNode) { - return [container, nextNode]; - } - } - - return [container, null]; - }, - - insertWidgetBefore: function(aNode, aNextNode, aContainer, aArea) { - this.notifyListeners("onWidgetBeforeDOMChange", aNode, aNextNode, aContainer); - this.setLocationAttributes(aNode, aArea); - aContainer.insertBefore(aNode, aNextNode); - this.notifyListeners("onWidgetAfterDOMChange", aNode, aNextNode, aContainer); - }, - - handleEvent: function(aEvent) { - switch (aEvent.type) { - case "command": - if (!this._originalEventInPanel(aEvent)) { - break; - } - aEvent = aEvent.sourceEvent; - // Fall through - case "click": - case "keypress": - this.maybeAutoHidePanel(aEvent); - break; - case "unload": - this.unregisterBuildWindow(aEvent.currentTarget); - break; - } - }, - - _originalEventInPanel: function(aEvent) { - let e = aEvent.sourceEvent; - if (!e) { - return false; - } - let node = this._getPanelForNode(e.target); - if (!node) { - return false; - } - let win = e.view; - let panels = gPanelsForWindow.get(win); - return !!panels && panels.has(node); - }, - - isSpecialWidget: function(aId) { - return (aId.startsWith(kSpecialWidgetPfx) || - aId.startsWith("separator") || - aId.startsWith("spring") || - aId.startsWith("spacer")); - }, - - ensureSpecialWidgetId: function(aId) { - let nodeType = aId.match(/spring|spacer|separator/)[0]; - // If the ID we were passed isn't a generated one, generate one now: - if (nodeType == aId) { - // Ids are differentiated through a unique count suffix. - return kSpecialWidgetPfx + aId + (++gNewElementCount); - } - return aId; - }, - - createSpecialWidget: function(aId, aDocument) { - let nodeName = "toolbar" + aId.match(/spring|spacer|separator/)[0]; - let node = aDocument.createElementNS(kNSXUL, nodeName); - node.id = this.ensureSpecialWidgetId(aId); - if (nodeName == "toolbarspring") { - node.flex = 1; - } - return node; - }, - - /* Find a XUL-provided widget in a window. Don't try to use this - * for an API-provided widget or a special widget. - */ - findWidgetInWindow: function(aId, aWindow) { - if (!gBuildWindows.has(aWindow)) { - throw new Error("Build window not registered"); - } - - if (!aId) { - log.error("findWidgetInWindow was passed an empty string."); - return null; - } - - let document = aWindow.document; - - // look for a node with the same id, as the node may be - // in a different toolbar. - let node = document.getElementById(aId); - if (node) { - let parent = node.parentNode; - while (parent && !(parent.customizationTarget || - parent == aWindow.gNavToolbox.palette)) { - parent = parent.parentNode; - } - - if (parent) { - let nodeInArea = node.parentNode.localName == "toolbarpaletteitem" ? - node.parentNode : node; - // Check if we're in a customization target, or in the palette: - if ((parent.customizationTarget == nodeInArea.parentNode && - gBuildWindows.get(aWindow).has(parent.toolbox)) || - aWindow.gNavToolbox.palette == nodeInArea.parentNode) { - // Normalize the removable attribute. For backwards compat, if - // the widget is not located in a toolbox palette then absence - // of the "removable" attribute means it is not removable. - if (!node.hasAttribute("removable")) { - // If we first see this in customization mode, it may be in the - // customization palette instead of the toolbox palette. - node.setAttribute("removable", !parent.customizationTarget); - } - return node; - } - } - } - - let toolboxes = gBuildWindows.get(aWindow); - for (let toolbox of toolboxes) { - if (toolbox.palette) { - // Attempt to locate a node with a matching ID within - // the palette. - let node = toolbox.palette.getElementsByAttribute("id", aId)[0]; - if (node) { - // Normalize the removable attribute. For backwards compat, this - // is optional if the widget is located in the toolbox palette, - // and defaults to *true*, unlike if it was located elsewhere. - if (!node.hasAttribute("removable")) { - node.setAttribute("removable", true); - } - return node; - } - } - } - return null; - }, - - buildWidget: function(aDocument, aWidget) { - if (aDocument.documentURI != kExpectedWindowURL) { - throw new Error("buildWidget was called for a non-browser window!"); - } - if (typeof aWidget == "string") { - aWidget = gPalette.get(aWidget); - } - if (!aWidget) { - throw new Error("buildWidget was passed a non-widget to build."); - } - - log.debug("Building " + aWidget.id + " of type " + aWidget.type); - - let node; - if (aWidget.type == "custom") { - if (aWidget.onBuild) { - node = aWidget.onBuild(aDocument); - } - if (!node || !(node instanceof aDocument.defaultView.XULElement)) - log.error("Custom widget with id " + aWidget.id + " does not return a valid node"); - } - else { - if (aWidget.onBeforeCreated) { - aWidget.onBeforeCreated(aDocument); - } - node = aDocument.createElementNS(kNSXUL, "toolbarbutton"); - - node.setAttribute("id", aWidget.id); - node.setAttribute("widget-id", aWidget.id); - node.setAttribute("widget-type", aWidget.type); - if (aWidget.disabled) { - node.setAttribute("disabled", true); - } - node.setAttribute("removable", aWidget.removable); - node.setAttribute("overflows", aWidget.overflows); - if (aWidget.tabSpecific) { - node.setAttribute("tabspecific", aWidget.tabSpecific); - } - node.setAttribute("label", this.getLocalizedProperty(aWidget, "label")); - let additionalTooltipArguments = []; - if (aWidget.shortcutId) { - let keyEl = aDocument.getElementById(aWidget.shortcutId); - if (keyEl) { - additionalTooltipArguments.push(ShortcutUtils.prettifyShortcut(keyEl)); - } else { - log.error("Key element with id '" + aWidget.shortcutId + "' for widget '" + aWidget.id + - "' not found!"); - } - } - - let tooltip = this.getLocalizedProperty(aWidget, "tooltiptext", additionalTooltipArguments); - if (tooltip) { - node.setAttribute("tooltiptext", tooltip); - } - node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional"); - - let commandHandler = this.handleWidgetCommand.bind(this, aWidget, node); - node.addEventListener("command", commandHandler, false); - let clickHandler = this.handleWidgetClick.bind(this, aWidget, node); - node.addEventListener("click", clickHandler, false); - - // If the widget has a view, and has view showing / hiding listeners, - // hook those up to this widget. - if (aWidget.type == "view") { - log.debug("Widget " + aWidget.id + " has a view. Auto-registering event handlers."); - let viewNode = aDocument.getElementById(aWidget.viewId); - - if (viewNode) { - // PanelUI relies on the .PanelUI-subView class to be able to show only - // one sub-view at a time. - viewNode.classList.add("PanelUI-subView"); - - for (let eventName of kSubviewEvents) { - let handler = "on" + eventName; - if (typeof aWidget[handler] == "function") { - viewNode.addEventListener(eventName, aWidget[handler], false); - } - } - - log.debug("Widget " + aWidget.id + " showing and hiding event handlers set."); - } else { - log.error("Could not find the view node with id: " + aWidget.viewId + - ", for widget: " + aWidget.id + "."); - } - } - - if (aWidget.onCreated) { - aWidget.onCreated(node); - } - } - - aWidget.instances.set(aDocument, node); - return node; - }, - - getLocalizedProperty: function(aWidget, aProp, aFormatArgs, aDef) { - const kReqStringProps = ["label"]; - - if (typeof aWidget == "string") { - aWidget = gPalette.get(aWidget); - } - if (!aWidget) { - throw new Error("getLocalizedProperty was passed a non-widget to work with."); - } - let def, name; - // Let widgets pass their own string identifiers or strings, so that - // we can use strings which aren't the default (in case string ids change) - // and so that non-builtin-widgets can also provide labels, tooltips, etc. - if (aWidget[aProp] != null) { - name = aWidget[aProp]; - // By using this as the default, if a widget provides a full string rather - // than a string ID for localization, we will fall back to that string - // and return that. - def = aDef || name; - } else { - name = aWidget.id + "." + aProp; - def = aDef || ""; - } - try { - if (Array.isArray(aFormatArgs) && aFormatArgs.length) { - return gWidgetsBundle.formatStringFromName(name, aFormatArgs, - aFormatArgs.length) || def; - } - return gWidgetsBundle.GetStringFromName(name) || def; - } catch (ex) { - // If an empty string was explicitly passed, treat it as an actual - // value rather than a missing property. - if (!def && (name != "" || kReqStringProps.includes(aProp))) { - log.error("Could not localize property '" + name + "'."); - } - } - return def; - }, - - addShortcut: function(aShortcutNode, aTargetNode) { - if (!aTargetNode) - aTargetNode = aShortcutNode; - let document = aShortcutNode.ownerDocument; - - // Detect if we've already been here before. - if (!aTargetNode || aTargetNode.hasAttribute("shortcut")) - return; - - let shortcutId = aShortcutNode.getAttribute("key"); - let shortcut; - if (shortcutId) { - shortcut = document.getElementById(shortcutId); - } else { - let commandId = aShortcutNode.getAttribute("command"); - if (commandId) - shortcut = ShortcutUtils.findShortcut(document.getElementById(commandId)); - } - if (!shortcut) { - return; - } - - aTargetNode.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(shortcut)); - }, - - handleWidgetCommand: function(aWidget, aNode, aEvent) { - log.debug("handleWidgetCommand"); - - if (aWidget.type == "button") { - if (aWidget.onCommand) { - try { - aWidget.onCommand.call(null, aEvent); - } catch (e) { - log.error(e); - } - } else { - // XXXunf Need to think this through more, and formalize. - Services.obs.notifyObservers(aNode, - "customizedui-widget-command", - aWidget.id); - } - } else if (aWidget.type == "view") { - let ownerWindow = aNode.ownerGlobal; - let area = this.getPlacementOfWidget(aNode.id).area; - let anchor = aNode; - if (area != CustomizableUI.AREA_PANEL) { - let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow); - if (wrapper && wrapper.anchor) { - this.hidePanelForNode(aNode); - anchor = wrapper.anchor; - } - } - ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, area); - } - }, - - handleWidgetClick: function(aWidget, aNode, aEvent) { - log.debug("handleWidgetClick"); - if (aWidget.onClick) { - try { - aWidget.onClick.call(null, aEvent); - } catch (e) { - Cu.reportError(e); - } - } else { - // XXXunf Need to think this through more, and formalize. - Services.obs.notifyObservers(aNode, "customizedui-widget-click", aWidget.id); - } - }, - - _getPanelForNode: function(aNode) { - let panel = aNode; - while (panel && panel.localName != "panel") - panel = panel.parentNode; - return panel; - }, - - /* - * If people put things in the panel which need more than single-click interaction, - * we don't want to close it. Right now we check for text inputs and menu buttons. - * We also check for being outside of any toolbaritem/toolbarbutton, ie on a blank - * part of the menu. - */ - _isOnInteractiveElement: function(aEvent) { - function getMenuPopupForDescendant(aNode) { - let lastPopup = null; - while (aNode && aNode.parentNode && - aNode.parentNode.localName.startsWith("menu")) { - lastPopup = aNode.localName == "menupopup" ? aNode : lastPopup; - aNode = aNode.parentNode; - } - return lastPopup; - } - - let target = aEvent.originalTarget; - let panel = this._getPanelForNode(aEvent.currentTarget); - // This can happen in e.g. customize mode. If there's no panel, - // there's clearly nothing for us to close; pretend we're interactive. - if (!panel) { - return true; - } - // We keep track of: - // whether we're in an input container (text field) - let inInput = false; - // whether we're in a popup/context menu - let inMenu = false; - // whether we're in a toolbarbutton/toolbaritem - let inItem = false; - // whether the current menuitem has a valid closemenu attribute - let menuitemCloseMenu = "auto"; - // whether the toolbarbutton/item has a valid closemenu attribute. - let closemenu = "auto"; - - // While keeping track of that, we go from the original target back up, - // to the panel if we have to. We bail as soon as we find an input, - // a toolbarbutton/item, or the panel: - while (true && target) { - // Skip out of iframes etc: - if (target.nodeType == target.DOCUMENT_NODE) { - if (!target.defaultView) { - // Err, we're done. - break; - } - // Cue some voodoo - target = target.defaultView.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIWebNavigation) - .QueryInterface(Ci.nsIDocShell) - .chromeEventHandler; - if (!target) { - break; - } - } - let tagName = target.localName; - inInput = tagName == "input" || tagName == "textbox"; - inItem = tagName == "toolbaritem" || tagName == "toolbarbutton"; - let isMenuItem = tagName == "menuitem"; - inMenu = inMenu || isMenuItem; - if (inItem && target.hasAttribute("closemenu")) { - let closemenuVal = target.getAttribute("closemenu"); - closemenu = (closemenuVal == "single" || closemenuVal == "none") ? - closemenuVal : "auto"; - } - - if (isMenuItem && target.hasAttribute("closemenu")) { - let closemenuVal = target.getAttribute("closemenu"); - menuitemCloseMenu = (closemenuVal == "single" || closemenuVal == "none") ? - closemenuVal : "auto"; - } - // Break out of the loop immediately for disabled items, as we need to - // keep the menu open in that case. - if (target.getAttribute("disabled") == "true") { - return true; - } - - // This isn't in the loop condition because we want to break before - // changing |target| if any of these conditions are true - if (inInput || inItem || target == panel) { - break; - } - // We need specific code for popups: the item on which they were invoked - // isn't necessarily in their parentNode chain: - if (isMenuItem) { - let topmostMenuPopup = getMenuPopupForDescendant(target); - target = (topmostMenuPopup && topmostMenuPopup.triggerNode) || - target.parentNode; - } else { - target = target.parentNode; - } - } - - // If the user clicked a menu item... - if (inMenu) { - // We care if we're in an input also, - // or if the user specified closemenu!="auto": - if (inInput || menuitemCloseMenu != "auto") { - return true; - } - // Otherwise, we're probably fine to close the panel - return false; - } - // If we're not in a menu, and we *are* in a type="menu" toolbarbutton, - // we'll now interact with the menu - if (inItem && target.getAttribute("type") == "menu") { - return true; - } - // If we're not in a menu, and we *are* in a type="menu-button" toolbarbutton, - // it depends whether we're in the dropmarker or the 'real' button: - if (inItem && target.getAttribute("type") == "menu-button") { - // 'real' button (which has a single action): - if (target.getAttribute("anonid") == "button") { - return closemenu != "none"; - } - // otherwise, this is the outer button, and the user will now - // interact with the menu: - return true; - } - return inInput || !inItem; - }, - - hidePanelForNode: function(aNode) { - let panel = this._getPanelForNode(aNode); - if (panel) { - panel.hidePopup(); - } - }, - - maybeAutoHidePanel: function(aEvent) { - if (aEvent.type == "keypress") { - if (aEvent.keyCode != aEvent.DOM_VK_RETURN) { - return; - } - // If the user hit enter/return, we don't check preventDefault - it makes sense - // that this was prevented, but we probably still want to close the panel. - // If consumers don't want this to happen, they should specify the closemenu - // attribute. - - } else if (aEvent.type != "command") { // mouse events: - if (aEvent.defaultPrevented || aEvent.button != 0) { - return; - } - let isInteractive = this._isOnInteractiveElement(aEvent); - log.debug("maybeAutoHidePanel: interactive ? " + isInteractive); - if (isInteractive) { - return; - } - } - - // We can't use event.target because we might have passed a panelview - // anonymous content boundary as well, and so target points to the - // panelmultiview in that case. Unfortunately, this means we get - // anonymous child nodes instead of the real ones, so looking for the - // 'stoooop, don't close me' attributes is more involved. - let target = aEvent.originalTarget; - let closemenu = "auto"; - let widgetType = "button"; - while (target.parentNode && target.localName != "panel") { - closemenu = target.getAttribute("closemenu"); - widgetType = target.getAttribute("widget-type"); - if (closemenu == "none" || closemenu == "single" || - widgetType == "view") { - break; - } - target = target.parentNode; - } - if (closemenu == "none" || widgetType == "view") { - return; - } - - if (closemenu == "single") { - let panel = this._getPanelForNode(target); - let multiview = panel.querySelector("panelmultiview"); - if (multiview.showingSubView) { - multiview.showMainView(); - return; - } - } - - // If we get here, we can actually hide the popup: - this.hidePanelForNode(aEvent.target); - }, - - getUnusedWidgets: function(aWindowPalette) { - let window = aWindowPalette.ownerGlobal; - let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window); - // We use a Set because there can be overlap between the widgets in - // gPalette and the items in the palette, especially after the first - // customization, since programmatically generated widgets will remain - // in the toolbox palette. - let widgets = new Set(); - - // It's possible that some widgets have been defined programmatically and - // have not been overlayed into the palette. We can find those inside - // gPalette. - for (let [id, widget] of gPalette) { - if (!widget.currentArea) { - if (widget.showInPrivateBrowsing || !isWindowPrivate) { - widgets.add(id); - } - } - } - - log.debug("Iterating the actual nodes of the window palette"); - for (let node of aWindowPalette.children) { - log.debug("In palette children: " + node.id); - if (node.id && !this.getPlacementOfWidget(node.id)) { - widgets.add(node.id); - } - } - - return [...widgets]; - }, - - getPlacementOfWidget: function(aWidgetId, aOnlyRegistered, aDeadAreas) { - if (aOnlyRegistered && !this.widgetExists(aWidgetId)) { - return null; - } - - for (let [area, placements] of gPlacements) { - if (!gAreas.has(area) && !aDeadAreas) { - continue; - } - let index = placements.indexOf(aWidgetId); - if (index != -1) { - return { area: area, position: index }; - } - } - - return null; - }, - - widgetExists: function(aWidgetId) { - if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) { - return true; - } - - // Destroyed API widgets are in gSeenWidgets, but not in gPalette: - if (gSeenWidgets.has(aWidgetId)) { - return false; - } - - // We're assuming XUL widgets always exist, as it's much harder to check, - // and checking would be much more error prone. - return true; - }, - - addWidgetToArea: function(aWidgetId, aArea, aPosition, aInitialAdd) { - if (!gAreas.has(aArea)) { - throw new Error("Unknown customization area: " + aArea); - } - - // Hack: don't want special widgets in the panel (need to check here as well - // as in canWidgetMoveToArea because the menu panel is lazy): - if (gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL && - this.isSpecialWidget(aWidgetId)) { - return; - } - - // If this is a lazy area that hasn't been restored yet, we can't yet modify - // it - would would at least like to add to it. So we keep track of it in - // gFuturePlacements, and use that to add it when restoring the area. We - // throw away aPosition though, as that can only be bogus if the area hasn't - // yet been restorted (caller can't possibly know where its putting the - // widget in relation to other widgets). - if (this.isAreaLazy(aArea)) { - gFuturePlacements.get(aArea).add(aWidgetId); - return; - } - - if (this.isSpecialWidget(aWidgetId)) { - aWidgetId = this.ensureSpecialWidgetId(aWidgetId); - } - - let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true); - if (oldPlacement && oldPlacement.area == aArea) { - this.moveWidgetWithinArea(aWidgetId, aPosition); - return; - } - - // Do nothing if the widget is not allowed to move to the target area. - if (!this.canWidgetMoveToArea(aWidgetId, aArea)) { - return; - } - - if (oldPlacement) { - this.removeWidgetFromArea(aWidgetId); - } - - if (!gPlacements.has(aArea)) { - gPlacements.set(aArea, [aWidgetId]); - aPosition = 0; - } else { - let placements = gPlacements.get(aArea); - if (typeof aPosition != "number") { - aPosition = placements.length; - } - if (aPosition < 0) { - aPosition = 0; - } - placements.splice(aPosition, 0, aWidgetId); - } - - let widget = gPalette.get(aWidgetId); - if (widget) { - widget.currentArea = aArea; - widget.currentPosition = aPosition; - } - - // We initially set placements with addWidgetToArea, so in that case - // we don't consider the area "dirtied". - if (!aInitialAdd) { - gDirtyAreaCache.add(aArea); - } - - gDirty = true; - this.saveState(); - - this.notifyListeners("onWidgetAdded", aWidgetId, aArea, aPosition); - }, - - removeWidgetFromArea: function(aWidgetId) { - let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true); - if (!oldPlacement) { - return; - } - - if (!this.isWidgetRemovable(aWidgetId)) { - return; - } - - let placements = gPlacements.get(oldPlacement.area); - let position = placements.indexOf(aWidgetId); - if (position != -1) { - placements.splice(position, 1); - } - - let widget = gPalette.get(aWidgetId); - if (widget) { - widget.currentArea = null; - widget.currentPosition = null; - } - - gDirty = true; - this.saveState(); - gDirtyAreaCache.add(oldPlacement.area); - - this.notifyListeners("onWidgetRemoved", aWidgetId, oldPlacement.area); - }, - - moveWidgetWithinArea: function(aWidgetId, aPosition) { - let oldPlacement = this.getPlacementOfWidget(aWidgetId); - if (!oldPlacement) { - return; - } - - let placements = gPlacements.get(oldPlacement.area); - if (typeof aPosition != "number") { - aPosition = placements.length; - } else if (aPosition < 0) { - aPosition = 0; - } else if (aPosition > placements.length) { - aPosition = placements.length; - } - - let widget = gPalette.get(aWidgetId); - if (widget) { - widget.currentPosition = aPosition; - widget.currentArea = oldPlacement.area; - } - - if (aPosition == oldPlacement.position) { - return; - } - - placements.splice(oldPlacement.position, 1); - // If we just removed the item from *before* where it is now added, - // we need to compensate the position offset for that: - if (oldPlacement.position < aPosition) { - aPosition--; - } - placements.splice(aPosition, 0, aWidgetId); - - gDirty = true; - gDirtyAreaCache.add(oldPlacement.area); - - this.saveState(); - - this.notifyListeners("onWidgetMoved", aWidgetId, oldPlacement.area, - oldPlacement.position, aPosition); - }, - - // Note that this does not populate gPlacements, which is done lazily so that - // the legacy state can be migrated, which is only available once a browser - // window is openned. - // The panel area is an exception here, since it has no legacy state and is - // built lazily - and therefore wouldn't otherwise result in restoring its - // state immediately when a browser window opens, which is important for - // other consumers of this API. - loadSavedState: function() { - let state = null; - try { - state = Services.prefs.getCharPref(kPrefCustomizationState); - } catch (e) { - log.debug("No saved state found"); - // This will fail if nothing has been customized, so silently fall back to - // the defaults. - } - - if (!state) { - return; - } - try { - gSavedState = JSON.parse(state); - if (typeof gSavedState != "object" || gSavedState === null) { - throw "Invalid saved state"; - } - } catch (e) { - Services.prefs.clearUserPref(kPrefCustomizationState); - gSavedState = {}; - log.debug("Error loading saved UI customization state, falling back to defaults."); - } - - if (!("placements" in gSavedState)) { - gSavedState.placements = {}; - } - - if (!("currentVersion" in gSavedState)) { - gSavedState.currentVersion = 0; - } - - gSeenWidgets = new Set(gSavedState.seen || []); - gDirtyAreaCache = new Set(gSavedState.dirtyAreaCache || []); - gNewElementCount = gSavedState.newElementCount || 0; - }, - - restoreStateForArea: function(aArea, aLegacyState) { - let placementsPreexisted = gPlacements.has(aArea); - - this.beginBatchUpdate(); - try { - gRestoring = true; - - let restored = false; - if (placementsPreexisted) { - log.debug("Restoring " + aArea + " from pre-existing placements"); - for (let [position, id] of gPlacements.get(aArea).entries()) { - this.moveWidgetWithinArea(id, position); - } - gDirty = false; - restored = true; - } else { - gPlacements.set(aArea, []); - } - - if (!restored && gSavedState && aArea in gSavedState.placements) { - log.debug("Restoring " + aArea + " from saved state"); - let placements = gSavedState.placements[aArea]; - for (let id of placements) - this.addWidgetToArea(id, aArea); - gDirty = false; - restored = true; - } - - if (!restored && aLegacyState) { - log.debug("Restoring " + aArea + " from legacy state"); - for (let id of aLegacyState) - this.addWidgetToArea(id, aArea); - // Don't override dirty state, to ensure legacy state is saved here and - // therefore only used once. - restored = true; - } - - if (!restored) { - log.debug("Restoring " + aArea + " from default state"); - let defaults = gAreas.get(aArea).get("defaultPlacements"); - if (defaults) { - for (let id of defaults) - this.addWidgetToArea(id, aArea, null, true); - } - gDirty = false; - } - - // Finally, add widgets to the area that were added before the it was able - // to be restored. This can occur when add-ons register widgets for a - // lazily-restored area before it's been restored. - if (gFuturePlacements.has(aArea)) { - for (let id of gFuturePlacements.get(aArea)) - this.addWidgetToArea(id, aArea); - gFuturePlacements.delete(aArea); - } - - log.debug("Placements for " + aArea + ":\n\t" + gPlacements.get(aArea).join("\n\t")); - - gRestoring = false; - } finally { - this.endBatchUpdate(); - } - }, - - saveState: function() { - if (gInBatchStack || !gDirty) { - return; - } - // Clone because we want to modify this map: - let state = { placements: new Map(gPlacements), - seen: gSeenWidgets, - dirtyAreaCache: gDirtyAreaCache, - currentVersion: kVersion, - newElementCount: gNewElementCount }; - - // Merge in previously saved areas if not present in gPlacements. - // This way, state is still persisted for e.g. temporarily disabled - // add-ons - see bug 989338. - if (gSavedState && gSavedState.placements) { - for (let area of Object.keys(gSavedState.placements)) { - if (!state.placements.has(area)) { - let placements = gSavedState.placements[area]; - state.placements.set(area, placements); - } - } - } - - log.debug("Saving state."); - let serialized = JSON.stringify(state, this.serializerHelper); - log.debug("State saved as: " + serialized); - Services.prefs.setCharPref(kPrefCustomizationState, serialized); - gDirty = false; - }, - - serializerHelper: function(aKey, aValue) { - if (typeof aValue == "object" && aValue.constructor.name == "Map") { - let result = {}; - for (let [mapKey, mapValue] of aValue) - result[mapKey] = mapValue; - return result; - } - - if (typeof aValue == "object" && aValue.constructor.name == "Set") { - return [...aValue]; - } - - return aValue; - }, - - beginBatchUpdate: function() { - gInBatchStack++; - }, - - endBatchUpdate: function(aForceDirty) { - gInBatchStack--; - if (aForceDirty === true) { - gDirty = true; - } - if (gInBatchStack == 0) { - this.saveState(); - } else if (gInBatchStack < 0) { - throw new Error("The batch editing stack should never reach a negative number."); - } - }, - - addListener: function(aListener) { - gListeners.add(aListener); - }, - - removeListener: function(aListener) { - if (aListener == this) { - return; - } - - gListeners.delete(aListener); - }, - - notifyListeners: function(aEvent, ...aArgs) { - if (gRestoring) { - return; - } - - for (let listener of gListeners) { - try { - if (typeof listener[aEvent] == "function") { - listener[aEvent].apply(listener, aArgs); - } - } catch (e) { - log.error(e + " -- " + e.fileName + ":" + e.lineNumber); - } - } - }, - - _dispatchToolboxEventToWindow: function(aEventType, aDetails, aWindow) { - let evt = new aWindow.CustomEvent(aEventType, { - bubbles: true, - cancelable: true, - detail: aDetails - }); - aWindow.gNavToolbox.dispatchEvent(evt); - }, - - dispatchToolboxEvent: function(aEventType, aDetails={}, aWindow=null) { - if (aWindow) { - this._dispatchToolboxEventToWindow(aEventType, aDetails, aWindow); - return; - } - for (let [win, ] of gBuildWindows) { - this._dispatchToolboxEventToWindow(aEventType, aDetails, win); - } - }, - - createWidget: function(aProperties) { - let widget = this.normalizeWidget(aProperties, CustomizableUI.SOURCE_EXTERNAL); - // XXXunf This should probably throw. - if (!widget) { - log.error("unable to normalize widget"); - return undefined; - } - - gPalette.set(widget.id, widget); - - // Clear our caches: - gGroupWrapperCache.delete(widget.id); - for (let [win, ] of gBuildWindows) { - let cache = gSingleWrapperCache.get(win); - if (cache) { - cache.delete(widget.id); - } - } - - this.notifyListeners("onWidgetCreated", widget.id); - - if (widget.defaultArea) { - let addToDefaultPlacements = false; - let area = gAreas.get(widget.defaultArea); - if (!CustomizableUI.isBuiltinToolbar(widget.defaultArea) && - widget.defaultArea != CustomizableUI.AREA_PANEL) { - addToDefaultPlacements = true; - } - - if (addToDefaultPlacements) { - if (area.has("defaultPlacements")) { - area.get("defaultPlacements").push(widget.id); - } else { - area.set("defaultPlacements", [widget.id]); - } - } - } - - // Look through previously saved state to see if we're restoring a widget. - let seenAreas = new Set(); - let widgetMightNeedAutoAdding = true; - for (let [area, ] of gPlacements) { - seenAreas.add(area); - let areaIsRegistered = gAreas.has(area); - let index = gPlacements.get(area).indexOf(widget.id); - if (index != -1) { - widgetMightNeedAutoAdding = false; - if (areaIsRegistered) { - widget.currentArea = area; - widget.currentPosition = index; - } - break; - } - } - - // Also look at saved state data directly in areas that haven't yet been - // restored. Can't rely on this for restored areas, as they may have - // changed. - if (widgetMightNeedAutoAdding && gSavedState) { - for (let area of Object.keys(gSavedState.placements)) { - if (seenAreas.has(area)) { - continue; - } - - let areaIsRegistered = gAreas.has(area); - let index = gSavedState.placements[area].indexOf(widget.id); - if (index != -1) { - widgetMightNeedAutoAdding = false; - if (areaIsRegistered) { - widget.currentArea = area; - widget.currentPosition = index; - } - break; - } - } - } - - // If we're restoring the widget to it's old placement, fire off the - // onWidgetAdded event - our own handler will take care of adding it to - // any build areas. - this.beginBatchUpdate(); - try { - if (widget.currentArea) { - this.notifyListeners("onWidgetAdded", widget.id, widget.currentArea, - widget.currentPosition); - } else if (widgetMightNeedAutoAdding) { - let autoAdd = true; - try { - autoAdd = Services.prefs.getBoolPref(kPrefCustomizationAutoAdd); - } catch (e) {} - - // If the widget doesn't have an existing placement, and it hasn't been - // seen before, then add it to its default area so it can be used. - // If the widget is not removable, we *have* to add it to its default - // area here. - let canBeAutoAdded = autoAdd && !gSeenWidgets.has(widget.id); - if (!widget.currentArea && (!widget.removable || canBeAutoAdded)) { - if (widget.defaultArea) { - if (this.isAreaLazy(widget.defaultArea)) { - gFuturePlacements.get(widget.defaultArea).add(widget.id); - } else { - this.addWidgetToArea(widget.id, widget.defaultArea); - } - } - } - } - } finally { - // Ensure we always have this widget in gSeenWidgets, and save - // state in case this needs to be done here. - gSeenWidgets.add(widget.id); - this.endBatchUpdate(true); - } - - this.notifyListeners("onWidgetAfterCreation", widget.id, widget.currentArea); - return widget.id; - }, - - createBuiltinWidget: function(aData) { - // This should only ever be called on startup, before any windows are - // opened - so we know there's no build areas to handle. Also, builtin - // widgets are expected to be (mostly) static, so shouldn't affect the - // current placement settings. - - // This allows a widget to be both built-in by default but also able to be - // destroyed and removed from the area based on criteria that may not be - // available when the widget is created -- for example, because some other - // feature in the browser supersedes the widget. - let conditionalDestroyPromise = aData.conditionalDestroyPromise || null; - delete aData.conditionalDestroyPromise; - - let widget = this.normalizeWidget(aData, CustomizableUI.SOURCE_BUILTIN); - if (!widget) { - log.error("Error creating builtin widget: " + aData.id); - return; - } - - log.debug("Creating built-in widget with id: " + widget.id); - gPalette.set(widget.id, widget); - - if (conditionalDestroyPromise) { - conditionalDestroyPromise.then(shouldDestroy => { - if (shouldDestroy) { - this.destroyWidget(widget.id); - this.removeWidgetFromArea(widget.id); - } - }, err => { - Cu.reportError(err); - }); - } - }, - - // Returns true if the area will eventually lazily restore (but hasn't yet). - isAreaLazy: function(aArea) { - if (gPlacements.has(aArea)) { - return false; - } - return gAreas.get(aArea).has("legacy"); - }, - - // XXXunf Log some warnings here, when the data provided isn't up to scratch. - normalizeWidget: function(aData, aSource) { - let widget = { - implementation: aData, - source: aSource || CustomizableUI.SOURCE_EXTERNAL, - instances: new Map(), - currentArea: null, - removable: true, - overflows: true, - defaultArea: null, - shortcutId: null, - tabSpecific: false, - tooltiptext: null, - showInPrivateBrowsing: true, - _introducedInVersion: -1, - }; - - if (typeof aData.id != "string" || !/^[a-z0-9-_]{1,}$/i.test(aData.id)) { - log.error("Given an illegal id in normalizeWidget: " + aData.id); - return null; - } - - delete widget.implementation.currentArea; - widget.implementation.__defineGetter__("currentArea", () => widget.currentArea); - - const kReqStringProps = ["id"]; - for (let prop of kReqStringProps) { - if (typeof aData[prop] != "string") { - log.error("Missing required property '" + prop + "' in normalizeWidget: " - + aData.id); - return null; - } - widget[prop] = aData[prop]; - } - - const kOptStringProps = ["label", "tooltiptext", "shortcutId"]; - for (let prop of kOptStringProps) { - if (typeof aData[prop] == "string") { - widget[prop] = aData[prop]; - } - } - - const kOptBoolProps = ["removable", "showInPrivateBrowsing", "overflows", "tabSpecific"]; - for (let prop of kOptBoolProps) { - if (typeof aData[prop] == "boolean") { - widget[prop] = aData[prop]; - } - } - - // When we normalize builtin widgets, areas have not yet been registered: - if (aData.defaultArea && - (aSource == CustomizableUI.SOURCE_BUILTIN || gAreas.has(aData.defaultArea))) { - widget.defaultArea = aData.defaultArea; - } else if (!widget.removable) { - log.error("Widget '" + widget.id + "' is not removable but does not specify " + - "a valid defaultArea. That's not possible; it must specify a " + - "valid defaultArea as well."); - return null; - } - - if ("type" in aData && gSupportedWidgetTypes.has(aData.type)) { - widget.type = aData.type; - } else { - widget.type = "button"; - } - - widget.disabled = aData.disabled === true; - - if (aSource == CustomizableUI.SOURCE_BUILTIN) { - widget._introducedInVersion = aData.introducedInVersion || 0; - } - - this.wrapWidgetEventHandler("onBeforeCreated", widget); - this.wrapWidgetEventHandler("onClick", widget); - this.wrapWidgetEventHandler("onCreated", widget); - this.wrapWidgetEventHandler("onDestroyed", widget); - - if (widget.type == "button") { - widget.onCommand = typeof aData.onCommand == "function" ? - aData.onCommand : - null; - } else if (widget.type == "view") { - if (typeof aData.viewId != "string") { - log.error("Expected a string for widget " + widget.id + " viewId, but got " - + aData.viewId); - return null; - } - widget.viewId = aData.viewId; - - this.wrapWidgetEventHandler("onViewShowing", widget); - this.wrapWidgetEventHandler("onViewHiding", widget); - } else if (widget.type == "custom") { - this.wrapWidgetEventHandler("onBuild", widget); - } - - if (gPalette.has(widget.id)) { - return null; - } - - return widget; - }, - - wrapWidgetEventHandler: function(aEventName, aWidget) { - if (typeof aWidget.implementation[aEventName] != "function") { - aWidget[aEventName] = null; - return; - } - aWidget[aEventName] = function(...aArgs) { - // Wrap inside a try...catch to properly log errors, until bug 862627 is - // fixed, which in turn might help bug 503244. - try { - // Don't copy the function to the normalized widget object, instead - // keep it on the original object provided to the API so that - // additional methods can be implemented and used by the event - // handlers. - return aWidget.implementation[aEventName].apply(aWidget.implementation, - aArgs); - } catch (e) { - Cu.reportError(e); - return undefined; - } - }; - }, - - destroyWidget: function(aWidgetId) { - let widget = gPalette.get(aWidgetId); - if (!widget) { - gGroupWrapperCache.delete(aWidgetId); - for (let [window, ] of gBuildWindows) { - let windowCache = gSingleWrapperCache.get(window); - if (windowCache) { - windowCache.delete(aWidgetId); - } - } - return; - } - - // Remove it from the default placements of an area if it was added there: - if (widget.defaultArea) { - let area = gAreas.get(widget.defaultArea); - if (area) { - let defaultPlacements = area.get("defaultPlacements"); - // We can assume this is present because if a widget has a defaultArea, - // we automatically create a defaultPlacements array for that area. - let widgetIndex = defaultPlacements.indexOf(aWidgetId); - if (widgetIndex != -1) { - defaultPlacements.splice(widgetIndex, 1); - } - } - } - - // This will not remove the widget from gPlacements - we want to keep the - // setting so the widget gets put back in it's old position if/when it - // returns. - for (let [window, ] of gBuildWindows) { - let windowCache = gSingleWrapperCache.get(window); - if (windowCache) { - windowCache.delete(aWidgetId); - } - let widgetNode = window.document.getElementById(aWidgetId) || - window.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0]; - if (widgetNode) { - let container = widgetNode.parentNode - this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null, - container, true); - widgetNode.remove(); - this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null, - container, true); - } - if (widget.type == "view") { - let viewNode = window.document.getElementById(widget.viewId); - if (viewNode) { - for (let eventName of kSubviewEvents) { - let handler = "on" + eventName; - if (typeof widget[handler] == "function") { - viewNode.removeEventListener(eventName, widget[handler], false); - } - } - } - } - if (widgetNode && widget.onDestroyed) { - widget.onDestroyed(window.document); - } - } - - gPalette.delete(aWidgetId); - gGroupWrapperCache.delete(aWidgetId); - - this.notifyListeners("onWidgetDestroyed", aWidgetId); - }, - - getCustomizeTargetForArea: function(aArea, aWindow) { - let buildAreaNodes = gBuildAreas.get(aArea); - if (!buildAreaNodes) { - return null; - } - - for (let node of buildAreaNodes) { - if (node.ownerGlobal == aWindow) { - return node.customizationTarget ? node.customizationTarget : node; - } - } - - return null; - }, - - reset: function() { - gResetting = true; - this._resetUIState(); - - // Rebuild each registered area (across windows) to reflect the state that - // was reset above. - this._rebuildRegisteredAreas(); - - for (let [widgetId, widget] of gPalette) { - if (widget.source == CustomizableUI.SOURCE_EXTERNAL) { - gSeenWidgets.add(widgetId); - } - } - if (gSeenWidgets.size) { - gDirty = true; - } - - gResetting = false; - }, - - _resetUIState: function() { - try { - gUIStateBeforeReset.drawInTitlebar = Services.prefs.getBoolPref(kPrefDrawInTitlebar); - gUIStateBeforeReset.uiCustomizationState = Services.prefs.getCharPref(kPrefCustomizationState); - gUIStateBeforeReset.currentTheme = LightweightThemeManager.currentTheme; - } catch (e) { } - - this._resetExtraToolbars(); - - Services.prefs.clearUserPref(kPrefCustomizationState); - Services.prefs.clearUserPref(kPrefDrawInTitlebar); - LightweightThemeManager.currentTheme = null; - log.debug("State reset"); - - // Reset placements to make restoring default placements possible. - gPlacements = new Map(); - gDirtyAreaCache = new Set(); - gSeenWidgets = new Set(); - // Clear the saved state to ensure that defaults will be used. - gSavedState = null; - // Restore the state for each area to its defaults - for (let [areaId, ] of gAreas) { - this.restoreStateForArea(areaId); - } - }, - - _resetExtraToolbars: function(aFilter = null) { - let firstWindow = true; // Only need to unregister and persist once - for (let [win, ] of gBuildWindows) { - let toolbox = win.gNavToolbox; - for (let child of toolbox.children) { - let matchesFilter = !aFilter || aFilter == child.id; - if (child.hasAttribute("customindex") && matchesFilter) { - let toolbarId = "toolbar" + child.getAttribute("customindex"); - toolbox.toolbarset.removeAttribute(toolbarId); - if (firstWindow) { - win.document.persist(toolbox.toolbarset.id, toolbarId); - // We have to unregister it properly to ensure we don't kill - // XUL widgets which might be in here - this.unregisterArea(child.id, true); - } - child.remove(); - } - } - firstWindow = false; - } - }, - - _rebuildRegisteredAreas: function() { - for (let [areaId, areaNodes] of gBuildAreas) { - let placements = gPlacements.get(areaId); - let isFirstChangedToolbar = true; - for (let areaNode of areaNodes) { - this.buildArea(areaId, placements, areaNode); - - let area = gAreas.get(areaId); - if (area.get("type") == CustomizableUI.TYPE_TOOLBAR) { - let defaultCollapsed = area.get("defaultCollapsed"); - let win = areaNode.ownerGlobal; - if (defaultCollapsed !== null) { - win.setToolbarVisibility(areaNode, !defaultCollapsed, isFirstChangedToolbar); - } - } - isFirstChangedToolbar = false; - } - } - }, - - /** - * Undoes a previous reset, restoring the state of the UI to the state prior to the reset. - */ - undoReset: function() { - if (gUIStateBeforeReset.uiCustomizationState == null || - gUIStateBeforeReset.drawInTitlebar == null) { - return; - } - gUndoResetting = true; - - let uiCustomizationState = gUIStateBeforeReset.uiCustomizationState; - let drawInTitlebar = gUIStateBeforeReset.drawInTitlebar; - let currentTheme = gUIStateBeforeReset.currentTheme; - - // Need to clear the previous state before setting the prefs - // because pref observers may check if there is a previous UI state. - this._clearPreviousUIState(); - - Services.prefs.setCharPref(kPrefCustomizationState, uiCustomizationState); - Services.prefs.setBoolPref(kPrefDrawInTitlebar, drawInTitlebar); - LightweightThemeManager.currentTheme = currentTheme; - this.loadSavedState(); - // If the user just customizes toolbar/titlebar visibility, gSavedState will be null - // and we don't need to do anything else here: - if (gSavedState) { - for (let areaId of Object.keys(gSavedState.placements)) { - let placements = gSavedState.placements[areaId]; - gPlacements.set(areaId, placements); - } - this._rebuildRegisteredAreas(); - } - - gUndoResetting = false; - }, - - _clearPreviousUIState: function() { - Object.getOwnPropertyNames(gUIStateBeforeReset).forEach((prop) => { - gUIStateBeforeReset[prop] = null; - }); - }, - - removeExtraToolbar: function(aToolbarId) { - this._resetExtraToolbars(aToolbarId); - }, - - /** - * @param {String|Node} aWidget - widget ID or a widget node (preferred for performance). - * @return {Boolean} whether the widget is removable - */ - isWidgetRemovable: function(aWidget) { - let widgetId; - let widgetNode; - if (typeof aWidget == "string") { - widgetId = aWidget; - } else { - widgetId = aWidget.id; - widgetNode = aWidget; - } - let provider = this.getWidgetProvider(widgetId); - - if (provider == CustomizableUI.PROVIDER_API) { - return gPalette.get(widgetId).removable; - } - - if (provider == CustomizableUI.PROVIDER_XUL) { - if (gBuildWindows.size == 0) { - // We don't have any build windows to look at, so just assume for now - // that its removable. - return true; - } - - if (!widgetNode) { - // Pick any of the build windows to look at. - let [window, ] = [...gBuildWindows][0]; - [, widgetNode] = this.getWidgetNode(widgetId, window); - } - // If we don't have a node, we assume it's removable. This can happen because - // getWidgetProvider returns PROVIDER_XUL by default, but this will also happen - // for API-provided widgets which have been destroyed. - if (!widgetNode) { - return true; - } - return widgetNode.getAttribute("removable") == "true"; - } - - // Otherwise this is either a special widget, which is always removable, or - // an API widget which has already been removed from gPalette. Returning true - // here allows us to then remove its ID from any placements where it might - // still occur. - return true; - }, - - canWidgetMoveToArea: function(aWidgetId, aArea) { - let placement = this.getPlacementOfWidget(aWidgetId); - if (placement && placement.area != aArea) { - // Special widgets can't move to the menu panel. - if (this.isSpecialWidget(aWidgetId) && gAreas.has(aArea) && - gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL) { - return false; - } - // For everything else, just return whether the widget is removable. - return this.isWidgetRemovable(aWidgetId); - } - - return true; - }, - - ensureWidgetPlacedInWindow: function(aWidgetId, aWindow) { - let placement = this.getPlacementOfWidget(aWidgetId); - if (!placement) { - return false; - } - let areaNodes = gBuildAreas.get(placement.area); - if (!areaNodes) { - return false; - } - let container = [...areaNodes].filter((n) => n.ownerGlobal == aWindow); - if (!container.length) { - return false; - } - let existingNode = container[0].getElementsByAttribute("id", aWidgetId)[0]; - if (existingNode) { - return true; - } - - this.insertNodeInWindow(aWidgetId, container[0], true); - return true; - }, - - get inDefaultState() { - for (let [areaId, props] of gAreas) { - let defaultPlacements = props.get("defaultPlacements"); - // Areas without default placements (like legacy ones?) get skipped - if (!defaultPlacements) { - continue; - } - - let currentPlacements = gPlacements.get(areaId); - // We're excluding all of the placement IDs for items that do not exist, - // and items that have removable="false", - // because we don't want to consider them when determining if we're - // in the default state. This way, if an add-on introduces a widget - // and is then uninstalled, the leftover placement doesn't cause us to - // automatically assume that the buttons are not in the default state. - let buildAreaNodes = gBuildAreas.get(areaId); - if (buildAreaNodes && buildAreaNodes.size) { - let container = [...buildAreaNodes][0]; - let removableOrDefault = (itemNodeOrItem) => { - let item = (itemNodeOrItem && itemNodeOrItem.id) || itemNodeOrItem; - let isRemovable = this.isWidgetRemovable(itemNodeOrItem); - let isInDefault = defaultPlacements.indexOf(item) != -1; - return isRemovable || isInDefault; - }; - // Toolbars have a currentSet property which also deals correctly with overflown - // widgets (if any) - use that instead: - if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { - let currentSet = container.currentSet; - currentPlacements = currentSet ? currentSet.split(',') : []; - currentPlacements = currentPlacements.filter(removableOrDefault); - } else { - // Clone the array so we don't modify the actual placements... - currentPlacements = [...currentPlacements]; - currentPlacements = currentPlacements.filter((item) => { - let itemNode = container.getElementsByAttribute("id", item)[0]; - return itemNode && removableOrDefault(itemNode || item); - }); - } - - if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { - let attribute = container.getAttribute("type") == "menubar" ? "autohide" : "collapsed"; - let collapsed = container.getAttribute(attribute) == "true"; - let defaultCollapsed = props.get("defaultCollapsed"); - if (defaultCollapsed !== null && collapsed != defaultCollapsed) { - log.debug("Found " + areaId + " had non-default toolbar visibility (expected " + defaultCollapsed + ", was " + collapsed + ")"); - return false; - } - } - } - log.debug("Checking default state for " + areaId + ":\n" + currentPlacements.join(",") + - "\nvs.\n" + defaultPlacements.join(",")); - - if (currentPlacements.length != defaultPlacements.length) { - return false; - } - - for (let i = 0; i < currentPlacements.length; ++i) { - if (currentPlacements[i] != defaultPlacements[i]) { - log.debug("Found " + currentPlacements[i] + " in " + areaId + " where " + - defaultPlacements[i] + " was expected!"); - return false; - } - } - } - - if (Services.prefs.prefHasUserValue(kPrefDrawInTitlebar)) { - log.debug(kPrefDrawInTitlebar + " pref is non-default"); - return false; - } - - if (LightweightThemeManager.currentTheme) { - log.debug(LightweightThemeManager.currentTheme + " theme is non-default"); - return false; - } - - return true; - }, - - setToolbarVisibility: function(aToolbarId, aIsVisible) { - // We only persist the attribute the first time. - let isFirstChangedToolbar = true; - for (let window of CustomizableUI.windows) { - let toolbar = window.document.getElementById(aToolbarId); - if (toolbar) { - window.setToolbarVisibility(toolbar, aIsVisible, isFirstChangedToolbar); - isFirstChangedToolbar = false; - } - } - }, -}; -Object.freeze(CustomizableUIInternal); - -this.CustomizableUI = { - /** - * Constant reference to the ID of the menu panel. - */ - AREA_PANEL: "PanelUI-contents", - /** - * Constant reference to the ID of the navigation toolbar. - */ - AREA_NAVBAR: "nav-bar", - /** - * Constant reference to the ID of the menubar's toolbar. - */ - AREA_MENUBAR: "toolbar-menubar", - /** - * Constant reference to the ID of the tabstrip toolbar. - */ - AREA_TABSTRIP: "TabsToolbar", - /** - * Constant reference to the ID of the bookmarks toolbar. - */ - AREA_BOOKMARKS: "PersonalToolbar", - /** - * Constant reference to the ID of the addon-bar toolbar shim. - * Do not use, this will be removed as soon as reasonably possible. - * @deprecated - */ - AREA_ADDONBAR: "addon-bar", - /** - * Constant indicating the area is a menu panel. - */ - TYPE_MENU_PANEL: "menu-panel", - /** - * Constant indicating the area is a toolbar. - */ - TYPE_TOOLBAR: "toolbar", - - /** - * Constant indicating a XUL-type provider. - */ - PROVIDER_XUL: "xul", - /** - * Constant indicating an API-type provider. - */ - PROVIDER_API: "api", - /** - * Constant indicating dynamic (special) widgets: spring, spacer, and separator. - */ - PROVIDER_SPECIAL: "special", - - /** - * Constant indicating the widget is built-in - */ - SOURCE_BUILTIN: "builtin", - /** - * Constant indicating the widget is externally provided - * (e.g. by add-ons or other items not part of the builtin widget set). - */ - SOURCE_EXTERNAL: "external", - - /** - * The class used to distinguish items that span the entire menu panel. - */ - WIDE_PANEL_CLASS: "panel-wide-item", - /** - * The (constant) number of columns in the menu panel. - */ - PANEL_COLUMN_COUNT: 3, - - /** - * Constant indicating the reason the event was fired was a window closing - */ - REASON_WINDOW_CLOSED: "window-closed", - /** - * Constant indicating the reason the event was fired was an area being - * unregistered separately from window closing mechanics. - */ - REASON_AREA_UNREGISTERED: "area-unregistered", - - - /** - * An iteratable property of windows managed by CustomizableUI. - * Note that this can *only* be used as an iterator. ie: - * for (let window of CustomizableUI.windows) { ... } - */ - windows: { - *[Symbol.iterator]() { - for (let [window, ] of gBuildWindows) - yield window; - } - }, - - /** - * Add a listener object that will get fired for various events regarding - * customization. - * - * @param aListener the listener object to add - * - * Not all event handler methods need to be defined. - * CustomizableUI will catch exceptions. Events are dispatched - * synchronously on the UI thread, so if you can delay any/some of your - * processing, that is advisable. The following event handlers are supported: - * - onWidgetAdded(aWidgetId, aArea, aPosition) - * Fired when a widget is added to an area. aWidgetId is the widget that - * was added, aArea the area it was added to, and aPosition the position - * in which it was added. - * - onWidgetMoved(aWidgetId, aArea, aOldPosition, aNewPosition) - * Fired when a widget is moved within its area. aWidgetId is the widget - * that was moved, aArea the area it was moved in, aOldPosition its old - * position, and aNewPosition its new position. - * - onWidgetRemoved(aWidgetId, aArea) - * Fired when a widget is removed from its area. aWidgetId is the widget - * that was removed, aArea the area it was removed from. - * - * - onWidgetBeforeDOMChange(aNode, aNextNode, aContainer, aIsRemoval) - * Fired *before* a widget's DOM node is acted upon by CustomizableUI - * (to add, move or remove it). aNode is the DOM node changed, aNextNode - * the DOM node (if any) before which a widget will be inserted, - * aContainer the *actual* DOM container (could be an overflow panel in - * case of an overflowable toolbar), and aWasRemoval is true iff the - * action about to happen is the removal of the DOM node. - * - onWidgetAfterDOMChange(aNode, aNextNode, aContainer, aWasRemoval) - * Like onWidgetBeforeDOMChange, but fired after the change to the DOM - * node of the widget. - * - * - onWidgetReset(aNode, aContainer) - * Fired after a reset to default placements moves a widget's node to a - * different location. aNode is the widget's node, aContainer is the - * area it was moved into (NB: it might already have been there and been - * moved to a different position!) - * - onWidgetUndoMove(aNode, aContainer) - * Fired after undoing a reset to default placements moves a widget's - * node to a different location. aNode is the widget's node, aContainer - * is the area it was moved into (NB: it might already have been there - * and been moved to a different position!) - * - onAreaReset(aArea, aContainer) - * Fired after a reset to default placements is complete on an area's - * DOM node. Note that this is fired for each DOM node. aArea is the area - * that was reset, aContainer the DOM node that was reset. - * - * - onWidgetCreated(aWidgetId) - * Fired when a widget with id aWidgetId has been created, but before it - * is added to any placements or any DOM nodes have been constructed. - * Only fired for API-based widgets. - * - onWidgetAfterCreation(aWidgetId, aArea) - * Fired after a widget with id aWidgetId has been created, and has been - * added to either its default area or the area in which it was placed - * previously. If the widget has no default area and/or it has never - * been placed anywhere, aArea may be null. Only fired for API-based - * widgets. - * - onWidgetDestroyed(aWidgetId) - * Fired when widgets are destroyed. aWidgetId is the widget that is - * being destroyed. Only fired for API-based widgets. - * - onWidgetInstanceRemoved(aWidgetId, aDocument) - * Fired when a window is unloaded and a widget's instance is destroyed - * because of this. Only fired for API-based widgets. - * - * - onWidgetDrag(aWidgetId, aArea) - * Fired both when and after customize mode drag handling system tries - * to determine the width and height of widget aWidgetId when dragged to a - * different area. aArea will be the area the item is dragged to, or - * undefined after the measurements have been done and the node has been - * moved back to its 'regular' area. - * - * - onCustomizeStart(aWindow) - * Fired when opening customize mode in aWindow. - * - onCustomizeEnd(aWindow) - * Fired when exiting customize mode in aWindow. - * - * - onWidgetOverflow(aNode, aContainer) - * Fired when a widget's DOM node is overflowing its container, a toolbar, - * and will be displayed in the overflow panel. - * - onWidgetUnderflow(aNode, aContainer) - * Fired when a widget's DOM node is *not* overflowing its container, a - * toolbar, anymore. - * - onWindowOpened(aWindow) - * Fired when a window has been opened that is managed by CustomizableUI, - * once all of the prerequisite setup has been done. - * - onWindowClosed(aWindow) - * Fired when a window that has been managed by CustomizableUI has been - * closed. - * - onAreaNodeRegistered(aArea, aContainer) - * Fired after an area node is first built when it is registered. This - * is often when the window has opened, but in the case of add-ons, - * could fire when the node has just been registered with CustomizableUI - * after an add-on update or disable/enable sequence. - * - onAreaNodeUnregistered(aArea, aContainer, aReason) - * Fired when an area node is explicitly unregistered by an API caller, - * or by a window closing. The aReason parameter indicates which of - * these is the case. - */ - addListener: function(aListener) { - CustomizableUIInternal.addListener(aListener); - }, - /** - * Remove a listener added with addListener - * @param aListener the listener object to remove - */ - removeListener: function(aListener) { - CustomizableUIInternal.removeListener(aListener); - }, - - /** - * Register a customizable area with CustomizableUI. - * @param aName the name of the area to register. Can only contain - * alphanumeric characters, dashes (-) and underscores (_). - * @param aProps the properties of the area. The following properties are - * recognized: - * - type: the type of area. Either TYPE_TOOLBAR (default) or - * TYPE_MENU_PANEL; - * - anchor: for a menu panel or overflowable toolbar, the - * anchoring node for the panel. - * - legacy: set to true if you want customizableui to - * automatically migrate the currentset attribute - * - overflowable: set to true if your toolbar is overflowable. - * This requires an anchor, and only has an - * effect for toolbars. - * - defaultPlacements: an array of widget IDs making up the - * default contents of the area - * - defaultCollapsed: (INTERNAL ONLY) applies if the type is TYPE_TOOLBAR, specifies - * if toolbar is collapsed by default (default to true). - * Specify null to ensure that reset/inDefaultArea don't care - * about a toolbar's collapsed state - */ - registerArea: function(aName, aProperties) { - CustomizableUIInternal.registerArea(aName, aProperties); - }, - /** - * Register a concrete node for a registered area. This method is automatically - * called from any toolbar in the main browser window that has its - * "customizable" attribute set to true. There should normally be no need to - * call it yourself. - * - * Note that ideally, you should register your toolbar using registerArea - * before any of the toolbars have their XBL bindings constructed (which - * will happen when they're added to the DOM and are not hidden). If you - * don't, and your toolbar has a defaultset attribute, CustomizableUI will - * register it automatically. If your toolbar does not have a defaultset - * attribute, the node will be saved for processing when you call - * registerArea. Note that CustomizableUI won't restore state in the area, - * allow the user to customize it in customize mode, or otherwise deal - * with it, until the area has been registered. - */ - registerToolbarNode: function(aToolbar, aExistingChildren) { - CustomizableUIInternal.registerToolbarNode(aToolbar, aExistingChildren); - }, - /** - * Register the menu panel node. This method should not be called by anyone - * apart from the built-in PanelUI. - * @param aPanel the panel DOM node being registered. - */ - registerMenuPanel: function(aPanel) { - CustomizableUIInternal.registerMenuPanel(aPanel); - }, - /** - * Unregister a customizable area. The inverse of registerArea. - * - * Unregistering an area will remove all the (removable) widgets in the - * area, which will return to the panel, and destroy all other traces - * of the area within CustomizableUI. Note that this means the *contents* - * of the area's DOM nodes will be moved to the panel or removed, but - * the area's DOM nodes *themselves* will stay. - * - * Furthermore, by default the placements of the area will be kept in the - * saved state (!) and restored if you re-register the area at a later - * point. This is useful for e.g. add-ons that get disabled and then - * re-enabled (e.g. when they update). - * - * You can override this last behaviour (and destroy the placements - * information in the saved state) by passing true for aDestroyPlacements. - * - * @param aName the name of the area to unregister - * @param aDestroyPlacements whether to destroy the placements information - * for the area, too. - */ - unregisterArea: function(aName, aDestroyPlacements) { - CustomizableUIInternal.unregisterArea(aName, aDestroyPlacements); - }, - /** - * Add a widget to an area. - * If the area to which you try to add is not known to CustomizableUI, - * this will throw. - * If the area to which you try to add has not yet been restored from its - * legacy state, this will postpone the addition. - * If the area to which you try to add is the same as the area in which - * the widget is currently placed, this will do the same as - * moveWidgetWithinArea. - * If the widget cannot be removed from its original location, this will - * no-op. - * - * This will fire an onWidgetAdded notification, - * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification - * for each window CustomizableUI knows about. - * - * @param aWidgetId the ID of the widget to add - * @param aArea the ID of the area to add the widget to - * @param aPosition the position at which to add the widget. If you do not - * pass a position, the widget will be added to the end - * of the area. - */ - addWidgetToArea: function(aWidgetId, aArea, aPosition) { - CustomizableUIInternal.addWidgetToArea(aWidgetId, aArea, aPosition); - }, - /** - * Remove a widget from its area. If the widget cannot be removed from its - * area, or is not in any area, this will no-op. Otherwise, this will fire an - * onWidgetRemoved notification, and an onWidgetBeforeDOMChange and - * onWidgetAfterDOMChange notification for each window CustomizableUI knows - * about. - * - * @param aWidgetId the ID of the widget to remove - */ - removeWidgetFromArea: function(aWidgetId) { - CustomizableUIInternal.removeWidgetFromArea(aWidgetId); - }, - /** - * Move a widget within an area. - * If the widget is not in any area, this will no-op. - * If the widget is already at the indicated position, this will no-op. - * - * Otherwise, this will move the widget and fire an onWidgetMoved notification, - * and an onWidgetBeforeDOMChange and onWidgetAfterDOMChange notification for - * each window CustomizableUI knows about. - * - * @param aWidgetId the ID of the widget to move - * @param aPosition the position to move the widget to. - * Negative values or values greater than the number of - * widgets will be interpreted to mean moving the widget to - * respectively the first or last position. - */ - moveWidgetWithinArea: function(aWidgetId, aPosition) { - CustomizableUIInternal.moveWidgetWithinArea(aWidgetId, aPosition); - }, - /** - * Ensure a XUL-based widget created in a window after areas were - * initialized moves to its correct position. - * This is roughly equivalent to manually looking up the position and using - * insertItem in the old API, but a lot less work for consumers. - * Always prefer this over using toolbar.insertItem (which might no-op - * because it delegates to addWidgetToArea) or, worse, moving items in the - * DOM yourself. - * - * @param aWidgetId the ID of the widget that was just created - * @param aWindow the window in which you want to ensure it was added. - * - * NB: why is this API per-window, you wonder? Because if you need this, - * presumably you yourself need to create the widget in all the windows - * and need to loop through them anyway. - */ - ensureWidgetPlacedInWindow: function(aWidgetId, aWindow) { - return CustomizableUIInternal.ensureWidgetPlacedInWindow(aWidgetId, aWindow); - }, - /** - * Start a batch update of items. - * During a batch update, the customization state is not saved to the user's - * preferences file, in order to reduce (possibly sync) IO. - * Calls to begin/endBatchUpdate may be nested. - * - * Callers should ensure that NO MATTER WHAT they call endBatchUpdate once - * for each call to beginBatchUpdate, even if there are exceptions in the - * code in the batch update. Otherwise, for the duration of the - * Firefox session, customization state is never saved. Typically, you - * would do this using a try...finally block. - */ - beginBatchUpdate: function() { - CustomizableUIInternal.beginBatchUpdate(); - }, - /** - * End a batch update. See the documentation for beginBatchUpdate above. - * - * State is not saved if we believe it is identical to the last known - * saved state. State is only ever saved when all batch updates have - * finished (ie there has been 1 endBatchUpdate call for each - * beginBatchUpdate call). If any of the endBatchUpdate calls pass - * aForceDirty=true, we will flush to the prefs file. - * - * @param aForceDirty force CustomizableUI to flush to the prefs file when - * all batch updates have finished. - */ - endBatchUpdate: function(aForceDirty) { - CustomizableUIInternal.endBatchUpdate(aForceDirty); - }, - /** - * Create a widget. - * - * To create a widget, you should pass an object with its desired - * properties. The following properties are supported: - * - * - id: the ID of the widget (required). - * - type: a string indicating the type of widget. Possible types - * are: - * 'button' - for simple button widgets (the default) - * 'view' - for buttons that open a panel or subview, - * depending on where they are placed. - * 'custom' - for fine-grained control over the creation - * of the widget. - * - viewId: Only useful for views (and required there): the id of the - * <panelview> that should be shown when clicking the widget. - * - onBuild(aDoc): Only useful for custom widgets (and required there); a - * function that will be invoked with the document in which - * to build a widget. Should return the DOM node that has - * been constructed. - * - onBeforeCreated(aDoc): Attached to all non-custom widgets; a function - * that will be invoked before the widget gets a DOM node - * constructed, passing the document in which that will happen. - * This is useful especially for 'view' type widgets that need - * to construct their views on the fly (e.g. from bootstrapped - * add-ons) - * - onCreated(aNode): Attached to all widgets; a function that will be invoked - * whenever the widget has a DOM node constructed, passing the - * constructed node as an argument. - * - onDestroyed(aDoc): Attached to all non-custom widgets; a function that - * will be invoked after the widget has a DOM node destroyed, - * passing the document from which it was removed. This is - * useful especially for 'view' type widgets that need to - * cleanup after views that were constructed on the fly. - * - onCommand(aEvt): Only useful for button widgets; a function that will be - * invoked when the user activates the button. - * - onClick(aEvt): Attached to all widgets; a function that will be invoked - * when the user clicks the widget. - * - onViewShowing(aEvt): Only useful for views; a function that will be - * invoked when a user shows your view. If any event - * handler calls aEvt.preventDefault(), the view will - * not be shown. - * - * The event's `detail` property is an object with an - * `addBlocker` method. Handlers which need to - * perform asynchronous operations before the view is - * shown may pass this method a Promise, which will - * prevent the view from showing until it resolves. - * Additionally, if the promise resolves to the exact - * value `false`, the view will not be shown. - * - onViewHiding(aEvt): Only useful for views; a function that will be - * invoked when a user hides your view. - * - tooltiptext: string to use for the tooltip of the widget - * - label: string to use for the label of the widget - * - removable: whether the widget is removable (optional, default: true) - * NB: if you specify false here, you must provide a - * defaultArea, too. - * - overflows: whether widget can overflow when in an overflowable - * toolbar (optional, default: true) - * - defaultArea: default area to add the widget to - * (optional, default: none; required if non-removable) - * - shortcutId: id of an element that has a shortcut for this widget - * (optional, default: null). This is only used to display - * the shortcut as part of the tooltip for builtin widgets - * (which have strings inside - * customizableWidgets.properties). If you're in an add-on, - * you should not set this property. - * - showInPrivateBrowsing: whether to show the widget in private browsing - * mode (optional, default: true) - * - * @param aProperties the specifications for the widget. - * @return a wrapper around the created widget (see getWidget) - */ - createWidget: function(aProperties) { - return CustomizableUIInternal.wrapWidget( - CustomizableUIInternal.createWidget(aProperties) - ); - }, - /** - * Destroy a widget - * - * If the widget is part of the default placements in an area, this will - * remove it from there. It will also remove any DOM instances. However, - * it will keep the widget in the placements for whatever area it was - * in at the time. You can remove it from there yourself by calling - * CustomizableUI.removeWidgetFromArea(aWidgetId). - * - * @param aWidgetId the ID of the widget to destroy - */ - destroyWidget: function(aWidgetId) { - CustomizableUIInternal.destroyWidget(aWidgetId); - }, - /** - * Get a wrapper object with information about the widget. - * The object provides the following properties - * (all read-only unless otherwise indicated): - * - * - id: the widget's ID; - * - type: the type of widget (button, view, custom). For - * XUL-provided widgets, this is always 'custom'; - * - provider: the provider type of the widget, id est one of - * PROVIDER_API or PROVIDER_XUL; - * - forWindow(w): a method to obtain a single window wrapper for a widget, - * in the window w passed as the only argument; - * - instances: an array of all instances (single window wrappers) - * of the widget. This array is NOT live; - * - areaType: the type of the widget's current area - * - isGroup: true; will be false for wrappers around single widget nodes; - * - source: for API-provided widgets, whether they are built-in to - * Firefox or add-on-provided; - * - disabled: for API-provided widgets, whether the widget is currently - * disabled. NB: this property is writable, and will toggle - * all the widgets' nodes' disabled states; - * - label: for API-provied widgets, the label of the widget; - * - tooltiptext: for API-provided widgets, the tooltip of the widget; - * - showInPrivateBrowsing: for API-provided widgets, whether the widget is - * visible in private browsing; - * - * Single window wrappers obtained through forWindow(someWindow) or from the - * instances array have the following properties - * (all read-only unless otherwise indicated): - * - * - id: the widget's ID; - * - type: the type of widget (button, view, custom). For - * XUL-provided widgets, this is always 'custom'; - * - provider: the provider type of the widget, id est one of - * PROVIDER_API or PROVIDER_XUL; - * - node: reference to the corresponding DOM node; - * - anchor: the anchor on which to anchor panels opened from this - * node. This will point to the overflow chevron on - * overflowable toolbars if and only if your widget node - * is overflowed, to the anchor for the panel menu - * if your widget is inside the panel menu, and to the - * node itself in all other cases; - * - overflowed: boolean indicating whether the node is currently in the - * overflow panel of the toolbar; - * - isGroup: false; will be true for the group widget; - * - label: for API-provided widgets, convenience getter for the - * label attribute of the DOM node; - * - tooltiptext: for API-provided widgets, convenience getter for the - * tooltiptext attribute of the DOM node; - * - disabled: for API-provided widgets, convenience getter *and setter* - * for the disabled state of this single widget. Note that - * you may prefer to use the group wrapper's getter/setter - * instead. - * - * @param aWidgetId the ID of the widget whose information you need - * @return a wrapper around the widget as described above, or null if the - * widget is known not to exist (anymore). NB: non-null return - * is no guarantee the widget exists because we cannot know in - * advance if a XUL widget exists or not. - */ - getWidget: function(aWidgetId) { - return CustomizableUIInternal.wrapWidget(aWidgetId); - }, - /** - * Get an array of widget wrappers (see getWidget) for all the widgets - * which are currently not in any area (so which are in the palette). - * - * @param aWindowPalette the palette (and by extension, the window) in which - * CustomizableUI should look. This matters because of - * course XUL-provided widgets could be available in - * some windows but not others, and likewise - * API-provided widgets might not exist in a private - * window (because of the showInPrivateBrowsing - * property). - * - * @return an array of widget wrappers (see getWidget) - */ - getUnusedWidgets: function(aWindowPalette) { - return CustomizableUIInternal.getUnusedWidgets(aWindowPalette).map( - CustomizableUIInternal.wrapWidget, - CustomizableUIInternal - ); - }, - /** - * Get an array of all the widget IDs placed in an area. This is roughly - * equivalent to fetching the currentset attribute and splitting by commas - * in the legacy APIs. Modifying the array will not affect CustomizableUI. - * - * @param aArea the ID of the area whose placements you want to obtain. - * @return an array containing the widget IDs that are in the area. - * - * NB: will throw if called too early (before placements have been fetched) - * or if the area is not currently known to CustomizableUI. - */ - getWidgetIdsInArea: function(aArea) { - if (!gAreas.has(aArea)) { - throw new Error("Unknown customization area: " + aArea); - } - if (!gPlacements.has(aArea)) { - throw new Error("Area not yet restored"); - } - - // We need to clone this, as we don't want to let consumers muck with placements - return [...gPlacements.get(aArea)]; - }, - /** - * Get an array of widget wrappers for all the widgets in an area. This is - * the same as calling getWidgetIdsInArea and .map() ing the result through - * CustomizableUI.getWidget. Careful: this means that if there are IDs in there - * which don't have corresponding DOM nodes (like in the old-style currentset - * attribute), there might be nulls in this array, or items for which - * wrapper.forWindow(win) will return null. - * - * @param aArea the ID of the area whose widgets you want to obtain. - * @return an array of widget wrappers and/or null values for the widget IDs - * placed in an area. - * - * NB: will throw if called too early (before placements have been fetched) - * or if the area is not currently known to CustomizableUI. - */ - getWidgetsInArea: function(aArea) { - return this.getWidgetIdsInArea(aArea).map( - CustomizableUIInternal.wrapWidget, - CustomizableUIInternal - ); - }, - /** - * Obtain an array of all the area IDs known to CustomizableUI. - * This array is created for you, so is modifiable without CustomizableUI - * being affected. - */ - get areas() { - return [...gAreas.keys()]; - }, - /** - * Check what kind of area (toolbar or menu panel) an area is. This is - * useful if you have a widget that needs to behave differently depending - * on its location. Note that widget wrappers have a convenience getter - * property (areaType) for this purpose. - * - * @param aArea the ID of the area whose type you want to know - * @return TYPE_TOOLBAR or TYPE_MENU_PANEL depending on the area, null if - * the area is unknown. - */ - getAreaType: function(aArea) { - let area = gAreas.get(aArea); - return area ? area.get("type") : null; - }, - /** - * Check if a toolbar is collapsed by default. - * - * @param aArea the ID of the area whose default-collapsed state you want to know. - * @return `true` or `false` depending on the area, null if the area is unknown, - * or its collapsed state cannot normally be controlled by the user - */ - isToolbarDefaultCollapsed: function(aArea) { - let area = gAreas.get(aArea); - return area ? area.get("defaultCollapsed") : null; - }, - /** - * Obtain the DOM node that is the customize target for an area in a - * specific window. - * - * Areas can have a customization target that does not correspond to the - * node itself. In particular, toolbars that have a customizationtarget - * attribute set will have their customization target set to that node. - * This means widgets will end up in the customization target, not in the - * DOM node with the ID that corresponds to the area ID. This is useful - * because it lets you have fixed content in a toolbar (e.g. the panel - * menu item in the navbar) and have all the customizable widgets use - * the customization target. - * - * Using this API yourself is discouraged; you should generally not need - * to be asking for the DOM container node used for a particular area. - * In particular, if you're wanting to check it in relation to a widget's - * node, your DOM node might not be a direct child of the customize target - * in a window if, for instance, the window is in customization mode, or if - * this is an overflowable toolbar and the widget has been overflowed. - * - * @param aArea the ID of the area whose customize target you want to have - * @param aWindow the window where you want to fetch the DOM node. - * @return the customize target DOM node for aArea in aWindow - */ - getCustomizeTargetForArea: function(aArea, aWindow) { - return CustomizableUIInternal.getCustomizeTargetForArea(aArea, aWindow); - }, - /** - * Reset the customization state back to its default. - * - * This is the nuclear option. You should never call this except if the user - * explicitly requests it. Firefox does this when the user clicks the - * "Restore Defaults" button in customize mode. - */ - reset: function() { - CustomizableUIInternal.reset(); - }, - - /** - * Undo the previous reset, can only be called immediately after a reset. - * @return a promise that will be resolved when the operation is complete. - */ - undoReset: function() { - CustomizableUIInternal.undoReset(); - }, - - /** - * Remove a custom toolbar added in a previous version of Firefox or using - * an add-on. NB: only works on the customizable toolbars generated by - * the toolbox itself. Intended for use from CustomizeMode, not by - * other consumers. - * @param aToolbarId the ID of the toolbar to remove - */ - removeExtraToolbar: function(aToolbarId) { - CustomizableUIInternal.removeExtraToolbar(aToolbarId); - }, - - /** - * Can the last Restore Defaults operation be undone. - * - * @return A boolean stating whether an undo of the - * Restore Defaults can be performed. - */ - get canUndoReset() { - return gUIStateBeforeReset.uiCustomizationState != null || - gUIStateBeforeReset.drawInTitlebar != null || - gUIStateBeforeReset.currentTheme != null; - }, - - /** - * Get the placement of a widget. This is by far the best way to obtain - * information about what the state of your widget is. The internals of - * this call are cheap (no DOM necessary) and you will know where the user - * has put your widget. - * - * @param aWidgetId the ID of the widget whose placement you want to know - * @return - * { - * area: "somearea", // The ID of the area where the widget is placed - * position: 42 // the index in the placements array corresponding to - * // your widget. - * } - * - * OR - * - * null // if the widget is not placed anywhere (ie in the palette) - */ - getPlacementOfWidget: function(aWidgetId, aOnlyRegistered=true, aDeadAreas=false) { - return CustomizableUIInternal.getPlacementOfWidget(aWidgetId, aOnlyRegistered, aDeadAreas); - }, - /** - * Check if a widget can be removed from the area it's in. - * - * Note that if you're wanting to move the widget somewhere, you should - * generally be checking canWidgetMoveToArea, because that will return - * true if the widget is already in the area where you want to move it (!). - * - * NB: oh, also, this method might lie if the widget in question is a - * XUL-provided widget and there are no windows open, because it - * can obviously not check anything in this case. It will return - * true. You will be able to move the widget elsewhere. However, - * once the user reopens a window, the widget will move back to its - * 'proper' area automagically. - * - * @param aWidgetId a widget ID or DOM node to check - * @return true if the widget can be removed from its area, - * false otherwise. - */ - isWidgetRemovable: function(aWidgetId) { - return CustomizableUIInternal.isWidgetRemovable(aWidgetId); - }, - /** - * Check if a widget can be moved to a particular area. Like - * isWidgetRemovable but better, because it'll return true if the widget - * is already in the right area. - * - * @param aWidgetId the widget ID or DOM node you want to move somewhere - * @param aArea the area ID you want to move it to. - * @return true if this is possible, false if it is not. The same caveats as - * for isWidgetRemovable apply, however, if no windows are open. - */ - canWidgetMoveToArea: function(aWidgetId, aArea) { - return CustomizableUIInternal.canWidgetMoveToArea(aWidgetId, aArea); - }, - /** - * Whether we're in a default state. Note that non-removable non-default - * widgets and non-existing widgets are not taken into account in determining - * whether we're in the default state. - * - * NB: this is a property with a getter. The getter is NOT cheap, because - * it does smart things with non-removable non-default items, non-existent - * items, and so forth. Please don't call unless necessary. - */ - get inDefaultState() { - return CustomizableUIInternal.inDefaultState; - }, - - /** - * Set a toolbar's visibility state in all windows. - * @param aToolbarId the toolbar whose visibility should be adjusted - * @param aIsVisible whether the toolbar should be visible - */ - setToolbarVisibility: function(aToolbarId, aIsVisible) { - CustomizableUIInternal.setToolbarVisibility(aToolbarId, aIsVisible); - }, - - /** - * Get a localized property off a (widget?) object. - * - * NB: this is unlikely to be useful unless you're in Firefox code, because - * this code uses the builtin widget stringbundle, and can't be told - * to use add-on-provided strings. It's mainly here as convenience for - * custom builtin widgets that build their own DOM but use the same - * stringbundle as the other builtin widgets. - * - * @param aWidget the object whose property we should use to fetch a - * localizable string; - * @param aProp the property on the object to use for the fetching; - * @param aFormatArgs (optional) any extra arguments to use for a formatted - * string; - * @param aDef (optional) the default to return if we don't find the - * string in the stringbundle; - * - * @return the localized string, or aDef if the string isn't in the bundle. - * If no default is provided, - * if aProp exists on aWidget, we'll return that, - * otherwise we'll return the empty string - * - */ - getLocalizedProperty: function(aWidget, aProp, aFormatArgs, aDef) { - return CustomizableUIInternal.getLocalizedProperty(aWidget, aProp, - aFormatArgs, aDef); - }, - /** - * Utility function to detect, find and set a keyboard shortcut for a menuitem - * or (toolbar)button. - * - * @param aShortcutNode the XUL node where the shortcut will be derived from; - * @param aTargetNode (optional) the XUL node on which the `shortcut` - * attribute will be set. If NULL, the shortcut will be - * set on aShortcutNode; - */ - addShortcut: function(aShortcutNode, aTargetNode) { - return CustomizableUIInternal.addShortcut(aShortcutNode, aTargetNode); - }, - /** - * Given a node, walk up to the first panel in its ancestor chain, and - * close it. - * - * @param aNode a node whose panel should be closed; - */ - hidePanelForNode: function(aNode) { - CustomizableUIInternal.hidePanelForNode(aNode); - }, - /** - * Check if a widget is a "special" widget: a spring, spacer or separator. - * - * @param aWidgetId the widget ID to check. - * @return true if the widget is 'special', false otherwise. - */ - isSpecialWidget: function(aWidgetId) { - return CustomizableUIInternal.isSpecialWidget(aWidgetId); - }, - /** - * Add listeners to a panel that will close it. For use from the menu panel - * and overflowable toolbar implementations, unlikely to be useful for - * consumers. - * - * @param aPanel the panel to which listeners should be attached. - */ - addPanelCloseListeners: function(aPanel) { - CustomizableUIInternal.addPanelCloseListeners(aPanel); - }, - /** - * Remove close listeners that have been added to a panel with - * addPanelCloseListeners. For use from the menu panel and overflowable - * toolbar implementations, unlikely to be useful for consumers. - * - * @param aPanel the panel from which listeners should be removed. - */ - removePanelCloseListeners: function(aPanel) { - CustomizableUIInternal.removePanelCloseListeners(aPanel); - }, - /** - * Notify listeners a widget is about to be dragged to an area. For use from - * Customize Mode only, do not use otherwise. - * - * @param aWidgetId the ID of the widget that is being dragged to an area. - * @param aArea the ID of the area to which the widget is being dragged. - */ - onWidgetDrag: function(aWidgetId, aArea) { - CustomizableUIInternal.notifyListeners("onWidgetDrag", aWidgetId, aArea); - }, - /** - * Notify listeners that a window is entering customize mode. For use from - * Customize Mode only, do not use otherwise. - * @param aWindow the window entering customize mode - */ - notifyStartCustomizing: function(aWindow) { - CustomizableUIInternal.notifyListeners("onCustomizeStart", aWindow); - }, - /** - * Notify listeners that a window is exiting customize mode. For use from - * Customize Mode only, do not use otherwise. - * @param aWindow the window exiting customize mode - */ - notifyEndCustomizing: function(aWindow) { - CustomizableUIInternal.notifyListeners("onCustomizeEnd", aWindow); - }, - - /** - * Notify toolbox(es) of a particular event. If you don't pass aWindow, - * all toolboxes will be notified. For use from Customize Mode only, - * do not use otherwise. - * @param aEvent the name of the event to send. - * @param aDetails optional, the details of the event. - * @param aWindow optional, the window in which to send the event. - */ - dispatchToolboxEvent: function(aEvent, aDetails={}, aWindow=null) { - CustomizableUIInternal.dispatchToolboxEvent(aEvent, aDetails, aWindow); - }, - - /** - * Check whether an area is overflowable. - * - * @param aAreaId the ID of an area to check for overflowable-ness - * @return true if the area is overflowable, false otherwise. - */ - isAreaOverflowable: function(aAreaId) { - let area = gAreas.get(aAreaId); - return area ? area.get("type") == this.TYPE_TOOLBAR && area.get("overflowable") - : false; - }, - /** - * Obtain a string indicating the place of an element. This is intended - * for use from customize mode; You should generally use getPlacementOfWidget - * instead, which is cheaper because it does not use the DOM. - * - * @param aElement the DOM node whose place we need to check - * @return "toolbar" if the node is in a toolbar, "panel" if it is in the - * menu panel, "palette" if it is in the (visible!) customization - * palette, undefined otherwise. - */ - getPlaceForItem: function(aElement) { - let place; - let node = aElement; - while (node && !place) { - if (node.localName == "toolbar") - place = "toolbar"; - else if (node.id == CustomizableUI.AREA_PANEL) - place = "panel"; - else if (node.id == "customization-palette") - place = "palette"; - - node = node.parentNode; - } - return place; - }, - - /** - * Check if a toolbar is builtin or not. - * @param aToolbarId the ID of the toolbar you want to check - */ - isBuiltinToolbar: function(aToolbarId) { - return CustomizableUIInternal._builtinToolbars.has(aToolbarId); - }, -}; -Object.freeze(this.CustomizableUI); -Object.freeze(this.CustomizableUI.windows); - -/** - * All external consumers of widgets are really interacting with these wrappers - * which provide a common interface. - */ - -/** - * WidgetGroupWrapper is the common interface for interacting with an entire - * widget group - AKA, all instances of a widget across a series of windows. - * This particular wrapper is only used for widgets created via the provider - * API. - */ -function WidgetGroupWrapper(aWidget) { - this.isGroup = true; - - const kBareProps = ["id", "source", "type", "disabled", "label", "tooltiptext", - "showInPrivateBrowsing", "viewId"]; - for (let prop of kBareProps) { - let propertyName = prop; - this.__defineGetter__(propertyName, () => aWidget[propertyName]); - } - - this.__defineGetter__("provider", () => CustomizableUI.PROVIDER_API); - - this.__defineSetter__("disabled", function(aValue) { - aValue = !!aValue; - aWidget.disabled = aValue; - for (let [, instance] of aWidget.instances) { - instance.disabled = aValue; - } - }); - - this.forWindow = function WidgetGroupWrapper_forWindow(aWindow) { - let wrapperMap; - if (!gSingleWrapperCache.has(aWindow)) { - wrapperMap = new Map(); - gSingleWrapperCache.set(aWindow, wrapperMap); - } else { - wrapperMap = gSingleWrapperCache.get(aWindow); - } - if (wrapperMap.has(aWidget.id)) { - return wrapperMap.get(aWidget.id); - } - - let instance = aWidget.instances.get(aWindow.document); - if (!instance && - (aWidget.showInPrivateBrowsing || !PrivateBrowsingUtils.isWindowPrivate(aWindow))) { - instance = CustomizableUIInternal.buildWidget(aWindow.document, - aWidget); - } - - let wrapper = new WidgetSingleWrapper(aWidget, instance); - wrapperMap.set(aWidget.id, wrapper); - return wrapper; - }; - - this.__defineGetter__("instances", function() { - // Can't use gBuildWindows here because some areas load lazily: - let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id); - if (!placement) { - return []; - } - let area = placement.area; - let buildAreas = gBuildAreas.get(area); - if (!buildAreas) { - return []; - } - return Array.from(buildAreas, (node) => this.forWindow(node.ownerGlobal)); - }); - - this.__defineGetter__("areaType", function() { - let areaProps = gAreas.get(aWidget.currentArea); - return areaProps && areaProps.get("type"); - }); - - Object.freeze(this); -} - -/** - * A WidgetSingleWrapper is a wrapper around a single instance of a widget in - * a particular window. - */ -function WidgetSingleWrapper(aWidget, aNode) { - this.isGroup = false; - - this.node = aNode; - this.provider = CustomizableUI.PROVIDER_API; - - const kGlobalProps = ["id", "type"]; - for (let prop of kGlobalProps) { - this[prop] = aWidget[prop]; - } - - const kNodeProps = ["label", "tooltiptext"]; - for (let prop of kNodeProps) { - let propertyName = prop; - // Look at the node for these, instead of the widget data, to ensure the - // wrapper always reflects this live instance. - this.__defineGetter__(propertyName, - () => aNode.getAttribute(propertyName)); - } - - this.__defineGetter__("disabled", () => aNode.disabled); - this.__defineSetter__("disabled", function(aValue) { - aNode.disabled = !!aValue; - }); - - this.__defineGetter__("anchor", function() { - let anchorId; - // First check for an anchor for the area: - let placement = CustomizableUIInternal.getPlacementOfWidget(aWidget.id); - if (placement) { - anchorId = gAreas.get(placement.area).get("anchor"); - } - if (!anchorId) { - anchorId = aNode.getAttribute("cui-anchorid"); - } - - return anchorId ? aNode.ownerDocument.getElementById(anchorId) - : aNode; - }); - - this.__defineGetter__("overflowed", function() { - return aNode.getAttribute("overflowedItem") == "true"; - }); - - Object.freeze(this); -} - -/** - * XULWidgetGroupWrapper is the common interface for interacting with an entire - * widget group - AKA, all instances of a widget across a series of windows. - * This particular wrapper is only used for widgets created via the old-school - * XUL method (overlays, or programmatically injecting toolbaritems, or other - * such things). - */ -// XXXunf Going to need to hook this up to some events to keep it all live. -function XULWidgetGroupWrapper(aWidgetId) { - this.isGroup = true; - this.id = aWidgetId; - this.type = "custom"; - this.provider = CustomizableUI.PROVIDER_XUL; - - this.forWindow = function XULWidgetGroupWrapper_forWindow(aWindow) { - let wrapperMap; - if (!gSingleWrapperCache.has(aWindow)) { - wrapperMap = new Map(); - gSingleWrapperCache.set(aWindow, wrapperMap); - } else { - wrapperMap = gSingleWrapperCache.get(aWindow); - } - if (wrapperMap.has(aWidgetId)) { - return wrapperMap.get(aWidgetId); - } - - let instance = aWindow.document.getElementById(aWidgetId); - if (!instance) { - // Toolbar palettes aren't part of the document, so elements in there - // won't be found via document.getElementById(). - instance = aWindow.gNavToolbox.palette.getElementsByAttribute("id", aWidgetId)[0]; - } - - let wrapper = new XULWidgetSingleWrapper(aWidgetId, instance, aWindow.document); - wrapperMap.set(aWidgetId, wrapper); - return wrapper; - }; - - this.__defineGetter__("areaType", function() { - let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId); - if (!placement) { - return null; - } - - let areaProps = gAreas.get(placement.area); - return areaProps && areaProps.get("type"); - }); - - this.__defineGetter__("instances", function() { - return Array.from(gBuildWindows, (wins) => this.forWindow(wins[0])); - }); - - Object.freeze(this); -} - -/** - * A XULWidgetSingleWrapper is a wrapper around a single instance of a XUL - * widget in a particular window. - */ -function XULWidgetSingleWrapper(aWidgetId, aNode, aDocument) { - this.isGroup = false; - - this.id = aWidgetId; - this.type = "custom"; - this.provider = CustomizableUI.PROVIDER_XUL; - - let weakDoc = Cu.getWeakReference(aDocument); - // If we keep a strong ref, the weak ref will never die, so null it out: - aDocument = null; - - this.__defineGetter__("node", function() { - // If we've set this to null (further down), we're sure there's nothing to - // be gotten here, so bail out early: - if (!weakDoc) { - return null; - } - if (aNode) { - // Return the last known node if it's still in the DOM... - if (aNode.ownerDocument.contains(aNode)) { - return aNode; - } - // ... or the toolbox - let toolbox = aNode.ownerGlobal.gNavToolbox; - if (toolbox && toolbox.palette && aNode.parentNode == toolbox.palette) { - return aNode; - } - // If it isn't, clear the cached value and fall through to the "slow" case: - aNode = null; - } - - let doc = weakDoc.get(); - if (doc) { - // Store locally so we can cache the result: - aNode = CustomizableUIInternal.findWidgetInWindow(aWidgetId, doc.defaultView); - return aNode; - } - // The weakref to the document is dead, we're done here forever more: - weakDoc = null; - return null; - }); - - this.__defineGetter__("anchor", function() { - let anchorId; - // First check for an anchor for the area: - let placement = CustomizableUIInternal.getPlacementOfWidget(aWidgetId); - if (placement) { - anchorId = gAreas.get(placement.area).get("anchor"); - } - - let node = this.node; - if (!anchorId && node) { - anchorId = node.getAttribute("cui-anchorid"); - } - - return (anchorId && node) ? node.ownerDocument.getElementById(anchorId) : node; - }); - - this.__defineGetter__("overflowed", function() { - let node = this.node; - if (!node) { - return false; - } - return node.getAttribute("overflowedItem") == "true"; - }); - - Object.freeze(this); -} - -const LAZY_RESIZE_INTERVAL_MS = 200; -const OVERFLOW_PANEL_HIDE_DELAY_MS = 500; - -function OverflowableToolbar(aToolbarNode) { - this._toolbar = aToolbarNode; - this._collapsed = new Map(); - this._enabled = true; - - this._toolbar.setAttribute("overflowable", "true"); - let doc = this._toolbar.ownerDocument; - this._target = this._toolbar.customizationTarget; - this._list = doc.getElementById(this._toolbar.getAttribute("overflowtarget")); - this._list.toolbox = this._toolbar.toolbox; - this._list.customizationTarget = this._list; - - let window = this._toolbar.ownerGlobal; - if (window.gBrowserInit.delayedStartupFinished) { - this.init(); - } else { - Services.obs.addObserver(this, "browser-delayed-startup-finished", false); - } -} - -OverflowableToolbar.prototype = { - initialized: false, - _forceOnOverflow: false, - - observe: function(aSubject, aTopic, aData) { - if (aTopic == "browser-delayed-startup-finished" && - aSubject == this._toolbar.ownerGlobal) { - Services.obs.removeObserver(this, "browser-delayed-startup-finished"); - this.init(); - } - }, - - init: function() { - let doc = this._toolbar.ownerDocument; - let window = doc.defaultView; - window.addEventListener("resize", this); - window.gNavToolbox.addEventListener("customizationstarting", this); - window.gNavToolbox.addEventListener("aftercustomization", this); - - let chevronId = this._toolbar.getAttribute("overflowbutton"); - this._chevron = doc.getElementById(chevronId); - this._chevron.addEventListener("command", this); - this._chevron.addEventListener("dragover", this); - this._chevron.addEventListener("dragend", this); - - let panelId = this._toolbar.getAttribute("overflowpanel"); - this._panel = doc.getElementById(panelId); - this._panel.addEventListener("popuphiding", this); - CustomizableUIInternal.addPanelCloseListeners(this._panel); - - CustomizableUI.addListener(this); - - // The 'overflow' event may have been fired before init was called. - if (this._toolbar.overflowedDuringConstruction) { - this.onOverflow(this._toolbar.overflowedDuringConstruction); - this._toolbar.overflowedDuringConstruction = null; - } - - this.initialized = true; - }, - - uninit: function() { - this._toolbar.removeEventListener("overflow", this._toolbar); - this._toolbar.removeEventListener("underflow", this._toolbar); - this._toolbar.removeAttribute("overflowable"); - - if (!this.initialized) { - Services.obs.removeObserver(this, "browser-delayed-startup-finished"); - return; - } - - this._disable(); - - let window = this._toolbar.ownerGlobal; - window.removeEventListener("resize", this); - window.gNavToolbox.removeEventListener("customizationstarting", this); - window.gNavToolbox.removeEventListener("aftercustomization", this); - this._chevron.removeEventListener("command", this); - this._chevron.removeEventListener("dragover", this); - this._chevron.removeEventListener("dragend", this); - this._panel.removeEventListener("popuphiding", this); - CustomizableUI.removeListener(this); - CustomizableUIInternal.removePanelCloseListeners(this._panel); - }, - - handleEvent: function(aEvent) { - switch (aEvent.type) { - case "aftercustomization": - this._enable(); - break; - case "command": - if (aEvent.target == this._chevron) { - this._onClickChevron(aEvent); - } else { - this._panel.hidePopup(); - } - break; - case "customizationstarting": - this._disable(); - break; - case "dragover": - this._showWithTimeout(); - break; - case "dragend": - this._panel.hidePopup(); - break; - case "popuphiding": - this._onPanelHiding(aEvent); - break; - case "resize": - this._onResize(aEvent); - } - }, - - show: function() { - if (this._panel.state == "open") { - return Promise.resolve(); - } - return new Promise(resolve => { - let doc = this._panel.ownerDocument; - this._panel.hidden = false; - let contextMenu = doc.getElementById(this._panel.getAttribute("context")); - gELS.addSystemEventListener(contextMenu, 'command', this, true); - let anchor = doc.getAnonymousElementByAttribute(this._chevron, "class", "toolbarbutton-icon"); - this._panel.openPopup(anchor || this._chevron); - this._chevron.open = true; - - let overflowableToolbarInstance = this; - this._panel.addEventListener("popupshown", function onPopupShown(aEvent) { - this.removeEventListener("popupshown", onPopupShown); - this.addEventListener("dragover", overflowableToolbarInstance); - this.addEventListener("dragend", overflowableToolbarInstance); - resolve(); - }); - }); - }, - - _onClickChevron: function(aEvent) { - if (this._chevron.open) { - this._panel.hidePopup(); - this._chevron.open = false; - } else { - this.show(); - } - }, - - _onPanelHiding: function(aEvent) { - this._chevron.open = false; - this._panel.removeEventListener("dragover", this); - this._panel.removeEventListener("dragend", this); - let doc = aEvent.target.ownerDocument; - let contextMenu = doc.getElementById(this._panel.getAttribute("context")); - gELS.removeSystemEventListener(contextMenu, 'command', this, true); - }, - - onOverflow: function(aEvent) { - // The rangeParent check is here because of bug 1111986 and ensuring that - // overflow events from the bookmarks toolbar items or similar things that - // manage their own overflow don't trigger an overflow on the entire toolbar - if (!this._enabled || - (aEvent && aEvent.target != this._toolbar.customizationTarget) || - (aEvent && aEvent.rangeParent)) - return; - - let child = this._target.lastChild; - - while (child && this._target.scrollLeftMin != this._target.scrollLeftMax) { - let prevChild = child.previousSibling; - - if (child.getAttribute("overflows") != "false") { - this._collapsed.set(child.id, this._target.clientWidth); - child.setAttribute("overflowedItem", true); - child.setAttribute("cui-anchorid", this._chevron.id); - CustomizableUIInternal.notifyListeners("onWidgetOverflow", child, this._target); - - this._list.insertBefore(child, this._list.firstChild); - if (!this._toolbar.hasAttribute("overflowing")) { - CustomizableUI.addListener(this); - } - this._toolbar.setAttribute("overflowing", "true"); - } - child = prevChild; - } - - let win = this._target.ownerGlobal; - win.UpdateUrlbarSearchSplitterState(); - }, - - _onResize: function(aEvent) { - if (!this._lazyResizeHandler) { - this._lazyResizeHandler = new DeferredTask(this._onLazyResize.bind(this), - LAZY_RESIZE_INTERVAL_MS); - } - this._lazyResizeHandler.arm(); - }, - - _moveItemsBackToTheirOrigin: function(shouldMoveAllItems) { - let placements = gPlacements.get(this._toolbar.id); - while (this._list.firstChild) { - let child = this._list.firstChild; - let minSize = this._collapsed.get(child.id); - - if (!shouldMoveAllItems && - minSize && - this._target.clientWidth <= minSize) { - return; - } - - this._collapsed.delete(child.id); - let beforeNodeIndex = placements.indexOf(child.id) + 1; - // If this is a skipintoolbarset item, meaning it doesn't occur in the placements list, - // we're inserting it at the end. This will mean first-in, first-out (more or less) - // leading to as little change in order as possible. - if (beforeNodeIndex == 0) { - beforeNodeIndex = placements.length; - } - let inserted = false; - for (; beforeNodeIndex < placements.length; beforeNodeIndex++) { - let beforeNode = this._target.getElementsByAttribute("id", placements[beforeNodeIndex])[0]; - if (beforeNode) { - this._target.insertBefore(child, beforeNode); - inserted = true; - break; - } - } - if (!inserted) { - this._target.appendChild(child); - } - child.removeAttribute("cui-anchorid"); - child.removeAttribute("overflowedItem"); - CustomizableUIInternal.notifyListeners("onWidgetUnderflow", child, this._target); - } - - let win = this._target.ownerGlobal; - win.UpdateUrlbarSearchSplitterState(); - - if (!this._collapsed.size) { - this._toolbar.removeAttribute("overflowing"); - CustomizableUI.removeListener(this); - } - }, - - _onLazyResize: function() { - if (!this._enabled) - return; - - if (this._target.scrollLeftMin != this._target.scrollLeftMax) { - this.onOverflow(); - } else { - this._moveItemsBackToTheirOrigin(); - } - }, - - _disable: function() { - this._enabled = false; - this._moveItemsBackToTheirOrigin(true); - if (this._lazyResizeHandler) { - this._lazyResizeHandler.disarm(); - } - }, - - _enable: function() { - this._enabled = true; - this.onOverflow(); - }, - - onWidgetBeforeDOMChange: function(aNode, aNextNode, aContainer) { - if (aContainer != this._target && aContainer != this._list) { - return; - } - // When we (re)move an item, update all the items that come after it in the list - // with the minsize *of the item before the to-be-removed node*. This way, we - // ensure that we try to move items back as soon as that's possible. - if (aNode.parentNode == this._list) { - let updatedMinSize; - if (aNode.previousSibling) { - updatedMinSize = this._collapsed.get(aNode.previousSibling.id); - } else { - // Force (these) items to try to flow back into the bar: - updatedMinSize = 1; - } - let nextItem = aNode.nextSibling; - while (nextItem) { - this._collapsed.set(nextItem.id, updatedMinSize); - nextItem = nextItem.nextSibling; - } - } - }, - - onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer) { - if (aContainer != this._target && aContainer != this._list) { - return; - } - - let nowInBar = aNode.parentNode == aContainer; - let nowOverflowed = aNode.parentNode == this._list; - let wasOverflowed = this._collapsed.has(aNode.id); - - // If this wasn't overflowed before... - if (!wasOverflowed) { - // ... but it is now, then we added to the overflow panel. Exciting stuff: - if (nowOverflowed) { - // NB: we're guaranteed that it has a previousSibling, because if it didn't, - // we would have added it to the toolbar instead. See getOverflowedNextNode. - let prevId = aNode.previousSibling.id; - let minSize = this._collapsed.get(prevId); - this._collapsed.set(aNode.id, minSize); - aNode.setAttribute("cui-anchorid", this._chevron.id); - aNode.setAttribute("overflowedItem", true); - CustomizableUIInternal.notifyListeners("onWidgetOverflow", aNode, this._target); - } - // If it is not overflowed and not in the toolbar, and was not overflowed - // either, it moved out of the toolbar. That means there's now space in there! - // Let's try to move stuff back: - else if (!nowInBar) { - this._moveItemsBackToTheirOrigin(true); - } - // If it's in the toolbar now, then we don't care. An overflow event may - // fire afterwards; that's ok! - } - // If it used to be overflowed... - else if (!nowOverflowed) { - // ... and isn't anymore, let's remove our bookkeeping: - this._collapsed.delete(aNode.id); - aNode.removeAttribute("cui-anchorid"); - aNode.removeAttribute("overflowedItem"); - CustomizableUIInternal.notifyListeners("onWidgetUnderflow", aNode, this._target); - - if (!this._collapsed.size) { - this._toolbar.removeAttribute("overflowing"); - CustomizableUI.removeListener(this); - } - } else if (aNode.previousSibling) { - // but if it still is, it must have changed places. Bookkeep: - let prevId = aNode.previousSibling.id; - let minSize = this._collapsed.get(prevId); - this._collapsed.set(aNode.id, minSize); - } else { - // If it's now the first item in the overflow list, - // maybe we can return it: - this._moveItemsBackToTheirOrigin(); - } - }, - - findOverflowedInsertionPoints: function(aNode) { - let newNodeCanOverflow = aNode.getAttribute("overflows") != "false"; - let areaId = this._toolbar.id; - let placements = gPlacements.get(areaId); - let nodeIndex = placements.indexOf(aNode.id); - let nodeBeforeNewNodeIsOverflown = false; - - let loopIndex = -1; - while (++loopIndex < placements.length) { - let nextNodeId = placements[loopIndex]; - if (loopIndex > nodeIndex) { - if (newNodeCanOverflow && this._collapsed.has(nextNodeId)) { - let nextNode = this._list.getElementsByAttribute("id", nextNodeId).item(0); - if (nextNode) { - return [this._list, nextNode]; - } - } - if (!nodeBeforeNewNodeIsOverflown || !newNodeCanOverflow) { - let nextNode = this._target.getElementsByAttribute("id", nextNodeId).item(0); - if (nextNode) { - return [this._target, nextNode]; - } - } - } else if (loopIndex < nodeIndex && this._collapsed.has(nextNodeId)) { - nodeBeforeNewNodeIsOverflown = true; - } - } - - let containerForAppending = (this._collapsed.size && newNodeCanOverflow) ? - this._list : this._target; - return [containerForAppending, null]; - }, - - getContainerFor: function(aNode) { - if (aNode.getAttribute("overflowedItem") == "true") { - return this._list; - } - return this._target; - }, - - _hideTimeoutId: null, - _showWithTimeout: function() { - this.show().then(function () { - let window = this._toolbar.ownerGlobal; - if (this._hideTimeoutId) { - window.clearTimeout(this._hideTimeoutId); - } - this._hideTimeoutId = window.setTimeout(() => { - if (!this._panel.firstChild.matches(":hover")) { - this._panel.hidePopup(); - } - }, OVERFLOW_PANEL_HIDE_DELAY_MS); - }.bind(this)); - }, -}; - -CustomizableUIInternal.initialize(); diff --git a/browser/components/customizableui/CustomizableWidgets.jsm b/browser/components/customizableui/CustomizableWidgets.jsm deleted file mode 100644 index 3e00d385f..000000000 --- a/browser/components/customizableui/CustomizableWidgets.jsm +++ /dev/null @@ -1,1219 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -this.EXPORTED_SYMBOLS = ["CustomizableWidgets"]; - -Cu.import("resource:///modules/CustomizableUI.jsm"); -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/AppConstants.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry", - "resource:///modules/BrowserUITelemetry.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PlacesUtils", - "resource://gre/modules/PlacesUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PlacesUIUtils", - "resource:///modules/PlacesUIUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "RecentlyClosedTabsAndWindowsMenuUtils", - "resource:///modules/sessionstore/RecentlyClosedTabsAndWindowsMenuUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils", - "resource://gre/modules/ShortcutUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "CharsetMenu", - "resource://gre/modules/CharsetMenu.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "PrivateBrowsingUtils", - "resource://gre/modules/PrivateBrowsingUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "SyncedTabs", - "resource://services-sync/SyncedTabs.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ContextualIdentityService", - "resource://gre/modules/ContextualIdentityService.jsm"); - -XPCOMUtils.defineLazyGetter(this, "CharsetBundle", function() { - const kCharsetBundle = "chrome://global/locale/charsetMenu.properties"; - return Services.strings.createBundle(kCharsetBundle); -}); -XPCOMUtils.defineLazyGetter(this, "BrandBundle", function() { - const kBrandBundle = "chrome://branding/locale/brand.properties"; - return Services.strings.createBundle(kBrandBundle); -}); - -const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; -const kPrefCustomizationDebug = "browser.uiCustomization.debug"; -const kWidePanelItemClass = "panel-wide-item"; - -XPCOMUtils.defineLazyGetter(this, "log", () => { - let scope = {}; - Cu.import("resource://gre/modules/Console.jsm", scope); - let debug; - try { - debug = Services.prefs.getBoolPref(kPrefCustomizationDebug); - } catch (ex) {} - let consoleOptions = { - maxLogLevel: debug ? "all" : "log", - prefix: "CustomizableWidgets", - }; - return new scope.ConsoleAPI(consoleOptions); -}); - - - -function setAttributes(aNode, aAttrs) { - let doc = aNode.ownerDocument; - for (let [name, value] of Object.entries(aAttrs)) { - if (!value) { - if (aNode.hasAttribute(name)) - aNode.removeAttribute(name); - } else { - if (name == "shortcutId") { - continue; - } - if (name == "label" || name == "tooltiptext") { - let stringId = (typeof value == "string") ? value : name; - let additionalArgs = []; - if (aAttrs.shortcutId) { - let shortcut = doc.getElementById(aAttrs.shortcutId); - if (shortcut) { - additionalArgs.push(ShortcutUtils.prettifyShortcut(shortcut)); - } - } - value = CustomizableUI.getLocalizedProperty({id: aAttrs.id}, stringId, additionalArgs); - } - aNode.setAttribute(name, value); - } - } -} - -function updateCombinedWidgetStyle(aNode, aArea, aModifyCloseMenu) { - let inPanel = (aArea == CustomizableUI.AREA_PANEL); - let cls = inPanel ? "panel-combined-button" : "toolbarbutton-1 toolbarbutton-combined"; - let attrs = {class: cls}; - if (aModifyCloseMenu) { - attrs.closemenu = inPanel ? "none" : null; - } - for (let i = 0, l = aNode.childNodes.length; i < l; ++i) { - if (aNode.childNodes[i].localName == "separator") - continue; - setAttributes(aNode.childNodes[i], attrs); - } -} - -function fillSubviewFromMenuItems(aMenuItems, aSubview) { - let attrs = ["oncommand", "onclick", "label", "key", "disabled", - "command", "observes", "hidden", "class", "origin", - "image", "checked"]; - - let doc = aSubview.ownerDocument; - let fragment = doc.createDocumentFragment(); - for (let menuChild of aMenuItems) { - if (menuChild.hidden) - continue; - - let subviewItem; - if (menuChild.localName == "menuseparator") { - // Don't insert duplicate or leading separators. This can happen if there are - // menus (which we don't copy) above the separator. - if (!fragment.lastChild || fragment.lastChild.localName == "menuseparator") { - continue; - } - subviewItem = doc.createElementNS(kNSXUL, "menuseparator"); - } else if (menuChild.localName == "menuitem") { - subviewItem = doc.createElementNS(kNSXUL, "toolbarbutton"); - CustomizableUI.addShortcut(menuChild, subviewItem); - - let item = menuChild; - if (!item.hasAttribute("onclick")) { - subviewItem.addEventListener("click", event => { - let newEvent = new doc.defaultView.MouseEvent(event.type, event); - item.dispatchEvent(newEvent); - }); - } - - if (!item.hasAttribute("oncommand")) { - subviewItem.addEventListener("command", event => { - let newEvent = doc.createEvent("XULCommandEvent"); - newEvent.initCommandEvent( - event.type, event.bubbles, event.cancelable, event.view, - event.detail, event.ctrlKey, event.altKey, event.shiftKey, - event.metaKey, event.sourceEvent); - item.dispatchEvent(newEvent); - }); - } - } else { - continue; - } - for (let attr of attrs) { - let attrVal = menuChild.getAttribute(attr); - if (attrVal) - subviewItem.setAttribute(attr, attrVal); - } - // We do this after so the .subviewbutton class doesn't get overriden. - if (menuChild.localName == "menuitem") { - subviewItem.classList.add("subviewbutton"); - } - fragment.appendChild(subviewItem); - } - aSubview.appendChild(fragment); -} - -function clearSubview(aSubview) { - let parent = aSubview.parentNode; - // We'll take the container out of the document before cleaning it out - // to avoid reflowing each time we remove something. - parent.removeChild(aSubview); - - while (aSubview.firstChild) { - aSubview.firstChild.remove(); - } - - parent.appendChild(aSubview); -} - -const CustomizableWidgets = [ - { - id: "history-panelmenu", - type: "view", - viewId: "PanelUI-history", - shortcutId: "key_gotoHistory", - tooltiptext: "history-panelmenu.tooltiptext2", - defaultArea: CustomizableUI.AREA_PANEL, - onViewShowing: function(aEvent) { - // Populate our list of history - const kMaxResults = 15; - let doc = aEvent.target.ownerDocument; - let win = doc.defaultView; - - let options = PlacesUtils.history.getNewQueryOptions(); - options.excludeQueries = true; - options.queryType = options.QUERY_TYPE_HISTORY; - options.sortingMode = options.SORT_BY_DATE_DESCENDING; - options.maxResults = kMaxResults; - let query = PlacesUtils.history.getNewQuery(); - - let items = doc.getElementById("PanelUI-historyItems"); - // Clear previous history items. - while (items.firstChild) { - items.firstChild.remove(); - } - - // Get all statically placed buttons to supply them with keyboard shortcuts. - let staticButtons = items.parentNode.getElementsByTagNameNS(kNSXUL, "toolbarbutton"); - for (let i = 0, l = staticButtons.length; i < l; ++i) - CustomizableUI.addShortcut(staticButtons[i]); - - PlacesUtils.history.QueryInterface(Ci.nsPIPlacesDatabase) - .asyncExecuteLegacyQueries([query], 1, options, { - handleResult: function (aResultSet) { - let onItemCommand = function (aEvent) { - // Only handle the click event for middle clicks, we're using the command - // event otherwise. - if (aEvent.type == "click" && aEvent.button != 1) { - return; - } - let item = aEvent.target; - win.openUILink(item.getAttribute("targetURI"), aEvent); - CustomizableUI.hidePanelForNode(item); - }; - let fragment = doc.createDocumentFragment(); - let row; - while ((row = aResultSet.getNextRow())) { - let uri = row.getResultByIndex(1); - let title = row.getResultByIndex(2); - let icon = row.getResultByIndex(6); - - let item = doc.createElementNS(kNSXUL, "toolbarbutton"); - item.setAttribute("label", title || uri); - item.setAttribute("targetURI", uri); - item.setAttribute("class", "subviewbutton"); - item.addEventListener("command", onItemCommand); - item.addEventListener("click", onItemCommand); - if (icon) { - let iconURL = "moz-anno:favicon:" + icon; - item.setAttribute("image", iconURL); - } - fragment.appendChild(item); - } - items.appendChild(fragment); - }, - handleError: function (aError) { - log.debug("History view tried to show but had an error: " + aError); - }, - handleCompletion: function (aReason) { - log.debug("History view is being shown!"); - }, - }); - - let recentlyClosedTabs = doc.getElementById("PanelUI-recentlyClosedTabs"); - while (recentlyClosedTabs.firstChild) { - recentlyClosedTabs.removeChild(recentlyClosedTabs.firstChild); - } - - let recentlyClosedWindows = doc.getElementById("PanelUI-recentlyClosedWindows"); - while (recentlyClosedWindows.firstChild) { - recentlyClosedWindows.removeChild(recentlyClosedWindows.firstChild); - } - - let utils = RecentlyClosedTabsAndWindowsMenuUtils; - let tabsFragment = utils.getTabsFragment(doc.defaultView, "toolbarbutton", true, - "menuRestoreAllTabsSubview.label"); - let separator = doc.getElementById("PanelUI-recentlyClosedTabs-separator"); - let elementCount = tabsFragment.childElementCount; - separator.hidden = !elementCount; - while (--elementCount >= 0) { - tabsFragment.children[elementCount].classList.add("subviewbutton", "cui-withicon"); - } - recentlyClosedTabs.appendChild(tabsFragment); - - let windowsFragment = utils.getWindowsFragment(doc.defaultView, "toolbarbutton", true, - "menuRestoreAllWindowsSubview.label"); - separator = doc.getElementById("PanelUI-recentlyClosedWindows-separator"); - elementCount = windowsFragment.childElementCount; - separator.hidden = !elementCount; - while (--elementCount >= 0) { - windowsFragment.children[elementCount].classList.add("subviewbutton", "cui-withicon"); - } - recentlyClosedWindows.appendChild(windowsFragment); - }, - onCreated: function(aNode) { - // Middle clicking recently closed items won't close the panel - cope: - let onRecentlyClosedClick = function(aEvent) { - if (aEvent.button == 1) { - CustomizableUI.hidePanelForNode(this); - } - }; - let doc = aNode.ownerDocument; - let recentlyClosedTabs = doc.getElementById("PanelUI-recentlyClosedTabs"); - let recentlyClosedWindows = doc.getElementById("PanelUI-recentlyClosedWindows"); - recentlyClosedTabs.addEventListener("click", onRecentlyClosedClick); - recentlyClosedWindows.addEventListener("click", onRecentlyClosedClick); - }, - onViewHiding: function(aEvent) { - log.debug("History view is being hidden!"); - } - }, { - id: "sync-button", - label: "remotetabs-panelmenu.label", - tooltiptext: "remotetabs-panelmenu.tooltiptext2", - type: "view", - viewId: "PanelUI-remotetabs", - defaultArea: CustomizableUI.AREA_PANEL, - deckIndices: { - DECKINDEX_TABS: 0, - DECKINDEX_TABSDISABLED: 1, - DECKINDEX_FETCHING: 2, - DECKINDEX_NOCLIENTS: 3, - }, - onCreated(aNode) { - // Add an observer to the button so we get the animation during sync. - // (Note the observer sets many attributes, including label and - // tooltiptext, but we only want the 'syncstatus' attribute for the - // animation) - let doc = aNode.ownerDocument; - let obnode = doc.createElementNS(kNSXUL, "observes"); - obnode.setAttribute("element", "sync-status"); - obnode.setAttribute("attribute", "syncstatus"); - aNode.appendChild(obnode); - - // A somewhat complicated dance to format the mobilepromo label. - let bundle = doc.getElementById("bundle_browser"); - let formatArgs = ["android", "ios"].map(os => { - let link = doc.createElement("label"); - link.textContent = bundle.getString(`appMenuRemoteTabs.mobilePromo.${os}`); - link.setAttribute("mobile-promo-os", os); - link.className = "text-link remotetabs-promo-link"; - return link.outerHTML; - }); - let promoParentElt = doc.getElementById("PanelUI-remotetabs-mobile-promo"); - // Put it all together... - let contents = bundle.getFormattedString("appMenuRemoteTabs.mobilePromo.text2", formatArgs); - promoParentElt.innerHTML = contents; - // We manually manage the "click" event to open the promo links because - // allowing the "text-link" widget handle it has 2 problems: (1) it only - // supports button 0 and (2) it's tricky to intercept when it does the - // open and auto-close the panel. (1) can probably be fixed, but (2) is - // trickier without hard-coding here the knowledge of exactly what buttons - // it does support. - // So we allow left and middle clicks to open the link in a new tab and - // close the panel; not setting a "href" attribute prevents the text-link - // widget handling it, and we build the final URL in the click handler to - // make testing easier (ie, so tests can change the pref after the links - // were created and have the new pref value used.) - promoParentElt.addEventListener("click", e => { - let os = e.target.getAttribute("mobile-promo-os"); - if (!os || e.button > 1) { - return; - } - let link = Services.prefs.getCharPref(`identity.mobilepromo.${os}`) + "synced-tabs"; - doc.defaultView.openUILinkIn(link, "tab"); - CustomizableUI.hidePanelForNode(e.target); - }); - }, - onViewShowing(aEvent) { - let doc = aEvent.target.ownerDocument; - this._tabsList = doc.getElementById("PanelUI-remotetabs-tabslist"); - Services.obs.addObserver(this, SyncedTabs.TOPIC_TABS_CHANGED, false); - - if (SyncedTabs.isConfiguredToSyncTabs) { - if (SyncedTabs.hasSyncedThisSession) { - this.setDeckIndex(this.deckIndices.DECKINDEX_TABS); - } else { - // Sync hasn't synced tabs yet, so show the "fetching" panel. - this.setDeckIndex(this.deckIndices.DECKINDEX_FETCHING); - } - // force a background sync. - SyncedTabs.syncTabs().catch(ex => { - Cu.reportError(ex); - }); - // show the current list - it will be updated by our observer. - this._showTabs(); - } else { - // not configured to sync tabs, so no point updating the list. - this.setDeckIndex(this.deckIndices.DECKINDEX_TABSDISABLED); - } - }, - onViewHiding() { - Services.obs.removeObserver(this, SyncedTabs.TOPIC_TABS_CHANGED); - this._tabsList = null; - }, - _tabsList: null, - observe(subject, topic, data) { - switch (topic) { - case SyncedTabs.TOPIC_TABS_CHANGED: - this._showTabs(); - break; - default: - break; - } - }, - setDeckIndex(index) { - let deck = this._tabsList.ownerDocument.getElementById("PanelUI-remotetabs-deck"); - // We call setAttribute instead of relying on the XBL property setter due - // to things going wrong when we try and set the index before the XBL - // binding has been created - see bug 1241851 for the gory details. - deck.setAttribute("selectedIndex", index); - }, - - _showTabsPromise: Promise.resolve(), - // Update the tab list after any existing in-flight updates are complete. - _showTabs() { - this._showTabsPromise = this._showTabsPromise.then(() => { - return this.__showTabs(); - }); - }, - // Return a new promise to update the tab list. - __showTabs() { - let doc = this._tabsList.ownerDocument; - return SyncedTabs.getTabClients().then(clients => { - // The view may have been hidden while the promise was resolving. - if (!this._tabsList) { - return; - } - if (clients.length === 0 && !SyncedTabs.hasSyncedThisSession) { - // the "fetching tabs" deck is being shown - let's leave it there. - // When that first sync completes we'll be notified and update. - return; - } - - if (clients.length === 0) { - this.setDeckIndex(this.deckIndices.DECKINDEX_NOCLIENTS); - return; - } - - this.setDeckIndex(this.deckIndices.DECKINDEX_TABS); - this._clearTabList(); - SyncedTabs.sortTabClientsByLastUsed(clients, 50 /* maxTabs */); - let fragment = doc.createDocumentFragment(); - - for (let client of clients) { - // add a menu separator for all clients other than the first. - if (fragment.lastChild) { - let separator = doc.createElementNS(kNSXUL, "menuseparator"); - fragment.appendChild(separator); - } - this._appendClient(client, fragment); - } - this._tabsList.appendChild(fragment); - }).catch(err => { - Cu.reportError(err); - }).then(() => { - // an observer for tests. - Services.obs.notifyObservers(null, "synced-tabs-menu:test:tabs-updated", null); - }); - }, - _clearTabList () { - let list = this._tabsList; - while (list.lastChild) { - list.lastChild.remove(); - } - }, - _showNoClientMessage() { - this._appendMessageLabel("notabslabel"); - }, - _appendMessageLabel(messageAttr, appendTo = null) { - if (!appendTo) { - appendTo = this._tabsList; - } - let message = this._tabsList.getAttribute(messageAttr); - let doc = this._tabsList.ownerDocument; - let messageLabel = doc.createElementNS(kNSXUL, "label"); - messageLabel.textContent = message; - appendTo.appendChild(messageLabel); - return messageLabel; - }, - _appendClient: function (client, attachFragment) { - let doc = attachFragment.ownerDocument; - // Create the element for the remote client. - let clientItem = doc.createElementNS(kNSXUL, "label"); - clientItem.setAttribute("itemtype", "client"); - let window = doc.defaultView; - clientItem.setAttribute("tooltiptext", - window.gSyncUI.formatLastSyncDate(new Date(client.lastModified))); - clientItem.textContent = client.name; - - attachFragment.appendChild(clientItem); - - if (client.tabs.length == 0) { - let label = this._appendMessageLabel("notabsforclientlabel", attachFragment); - label.setAttribute("class", "PanelUI-remotetabs-notabsforclient-label"); - } else { - for (let tab of client.tabs) { - let tabEnt = this._createTabElement(doc, tab); - attachFragment.appendChild(tabEnt); - } - } - }, - _createTabElement(doc, tabInfo) { - let item = doc.createElementNS(kNSXUL, "toolbarbutton"); - let tooltipText = (tabInfo.title ? tabInfo.title + "\n" : "") + tabInfo.url; - item.setAttribute("itemtype", "tab"); - item.setAttribute("class", "subviewbutton"); - item.setAttribute("targetURI", tabInfo.url); - item.setAttribute("label", tabInfo.title != "" ? tabInfo.title : tabInfo.url); - item.setAttribute("image", tabInfo.icon); - item.setAttribute("tooltiptext", tooltipText); - // We need to use "click" instead of "command" here so openUILink - // respects different buttons (eg, to open in a new tab). - item.addEventListener("click", e => { - doc.defaultView.openUILink(tabInfo.url, e); - CustomizableUI.hidePanelForNode(item); - BrowserUITelemetry.countSyncedTabEvent("open", "toolbarbutton-subview"); - }); - return item; - }, - }, { - id: "privatebrowsing-button", - shortcutId: "key_privatebrowsing", - defaultArea: CustomizableUI.AREA_PANEL, - onCommand: function(e) { - let win = e.target.ownerGlobal; - win.OpenBrowserWindow({private: true}); - } - }, { - id: "save-page-button", - shortcutId: "key_savePage", - tooltiptext: "save-page-button.tooltiptext3", - defaultArea: CustomizableUI.AREA_PANEL, - onCommand: function(aEvent) { - let win = aEvent.target.ownerGlobal; - win.saveBrowser(win.gBrowser.selectedBrowser); - } - }, { - id: "find-button", - shortcutId: "key_find", - tooltiptext: "find-button.tooltiptext3", - defaultArea: CustomizableUI.AREA_PANEL, - onCommand: function(aEvent) { - let win = aEvent.target.ownerGlobal; - if (win.gFindBar) { - win.gFindBar.onFindCommand(); - } - } - }, { - id: "open-file-button", - shortcutId: "openFileKb", - tooltiptext: "open-file-button.tooltiptext3", - defaultArea: CustomizableUI.AREA_PANEL, - onCommand: function(aEvent) { - let win = aEvent.target.ownerGlobal; - win.BrowserOpenFileWindow(); - } - }, { - id: "sidebar-button", - type: "view", - viewId: "PanelUI-sidebar", - tooltiptext: "sidebar-button.tooltiptext2", - onViewShowing: function(aEvent) { - // Populate the subview with whatever menuitems are in the - // sidebar menu. We skip menu elements, because the menu panel has no way - // of dealing with those right now. - let doc = aEvent.target.ownerDocument; - let menu = doc.getElementById("viewSidebarMenu"); - - // First clear any existing menuitems then populate. Add it to the - // standard menu first, then copy all sidebar options to the panel. - let sidebarItems = doc.getElementById("PanelUI-sidebarItems"); - clearSubview(sidebarItems); - fillSubviewFromMenuItems([...menu.children], sidebarItems); - } - }, { - id: "add-ons-button", - shortcutId: "key_openAddons", - tooltiptext: "add-ons-button.tooltiptext3", - defaultArea: CustomizableUI.AREA_PANEL, - onCommand: function(aEvent) { - let win = aEvent.target.ownerGlobal; - win.BrowserOpenAddonsMgr(); - } - }, { - id: "zoom-controls", - type: "custom", - tooltiptext: "zoom-controls.tooltiptext2", - defaultArea: CustomizableUI.AREA_PANEL, - onBuild: function(aDocument) { - const kPanelId = "PanelUI-popup"; - let areaType = CustomizableUI.getAreaType(this.currentArea); - let inPanel = areaType == CustomizableUI.TYPE_MENU_PANEL; - let inToolbar = areaType == CustomizableUI.TYPE_TOOLBAR; - - let buttons = [{ - id: "zoom-out-button", - command: "cmd_fullZoomReduce", - label: true, - tooltiptext: "tooltiptext2", - shortcutId: "key_fullZoomReduce", - }, { - id: "zoom-reset-button", - command: "cmd_fullZoomReset", - tooltiptext: "tooltiptext2", - shortcutId: "key_fullZoomReset", - }, { - id: "zoom-in-button", - command: "cmd_fullZoomEnlarge", - label: true, - tooltiptext: "tooltiptext2", - shortcutId: "key_fullZoomEnlarge", - }]; - - let node = aDocument.createElementNS(kNSXUL, "toolbaritem"); - node.setAttribute("id", "zoom-controls"); - node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label")); - node.setAttribute("title", CustomizableUI.getLocalizedProperty(this, "tooltiptext")); - // Set this as an attribute in addition to the property to make sure we can style correctly. - node.setAttribute("removable", "true"); - node.classList.add("chromeclass-toolbar-additional"); - node.classList.add("toolbaritem-combined-buttons"); - node.classList.add(kWidePanelItemClass); - - buttons.forEach(function(aButton, aIndex) { - if (aIndex != 0) - node.appendChild(aDocument.createElementNS(kNSXUL, "separator")); - let btnNode = aDocument.createElementNS(kNSXUL, "toolbarbutton"); - setAttributes(btnNode, aButton); - node.appendChild(btnNode); - }); - - // The middle node is the 'Reset Zoom' button. - let zoomResetButton = node.childNodes[2]; - let window = aDocument.defaultView; - function updateZoomResetButton() { - let updateDisplay = true; - // Label should always show 100% in customize mode, so don't update: - if (aDocument.documentElement.hasAttribute("customizing")) { - updateDisplay = false; - } - // XXXgijs in some tests we get called very early, and there's no docShell on the - // tabbrowser. This breaks the zoom toolkit code (see bug 897410). Don't let that happen: - let zoomFactor = 100; - try { - zoomFactor = Math.round(window.ZoomManager.zoom * 100); - } catch (e) {} - zoomResetButton.setAttribute("label", CustomizableUI.getLocalizedProperty( - buttons[1], "label", [updateDisplay ? zoomFactor : 100] - )); - } - - // Register ourselves with the service so we know when the zoom prefs change. - Services.obs.addObserver(updateZoomResetButton, "browser-fullZoom:zoomChange", false); - Services.obs.addObserver(updateZoomResetButton, "browser-fullZoom:zoomReset", false); - Services.obs.addObserver(updateZoomResetButton, "browser-fullZoom:location-change", false); - - if (inPanel) { - let panel = aDocument.getElementById(kPanelId); - panel.addEventListener("popupshowing", updateZoomResetButton); - } else { - if (inToolbar) { - let container = window.gBrowser.tabContainer; - container.addEventListener("TabSelect", updateZoomResetButton); - } - updateZoomResetButton(); - } - updateCombinedWidgetStyle(node, this.currentArea, true); - - let listener = { - onWidgetAdded: function(aWidgetId, aArea, aPosition) { - if (aWidgetId != this.id) - return; - - updateCombinedWidgetStyle(node, aArea, true); - updateZoomResetButton(); - - let areaType = CustomizableUI.getAreaType(aArea); - if (areaType == CustomizableUI.TYPE_MENU_PANEL) { - let panel = aDocument.getElementById(kPanelId); - panel.addEventListener("popupshowing", updateZoomResetButton); - } else if (areaType == CustomizableUI.TYPE_TOOLBAR) { - let container = window.gBrowser.tabContainer; - container.addEventListener("TabSelect", updateZoomResetButton); - } - }.bind(this), - - onWidgetRemoved: function(aWidgetId, aPrevArea) { - if (aWidgetId != this.id) - return; - - let areaType = CustomizableUI.getAreaType(aPrevArea); - if (areaType == CustomizableUI.TYPE_MENU_PANEL) { - let panel = aDocument.getElementById(kPanelId); - panel.removeEventListener("popupshowing", updateZoomResetButton); - } else if (areaType == CustomizableUI.TYPE_TOOLBAR) { - let container = window.gBrowser.tabContainer; - container.removeEventListener("TabSelect", updateZoomResetButton); - } - - // When a widget is demoted to the palette ('removed'), it's visual - // style should change. - updateCombinedWidgetStyle(node, null, true); - updateZoomResetButton(); - }.bind(this), - - onWidgetReset: function(aWidgetNode) { - if (aWidgetNode != node) - return; - updateCombinedWidgetStyle(node, this.currentArea, true); - updateZoomResetButton(); - }.bind(this), - - onWidgetUndoMove: function(aWidgetNode) { - if (aWidgetNode != node) - return; - updateCombinedWidgetStyle(node, this.currentArea, true); - updateZoomResetButton(); - }.bind(this), - - onWidgetMoved: function(aWidgetId, aArea) { - if (aWidgetId != this.id) - return; - updateCombinedWidgetStyle(node, aArea, true); - updateZoomResetButton(); - }.bind(this), - - onWidgetInstanceRemoved: function(aWidgetId, aDoc) { - if (aWidgetId != this.id || aDoc != aDocument) - return; - - CustomizableUI.removeListener(listener); - Services.obs.removeObserver(updateZoomResetButton, "browser-fullZoom:zoomChange"); - Services.obs.removeObserver(updateZoomResetButton, "browser-fullZoom:zoomReset"); - Services.obs.removeObserver(updateZoomResetButton, "browser-fullZoom:location-change"); - let panel = aDoc.getElementById(kPanelId); - panel.removeEventListener("popupshowing", updateZoomResetButton); - let container = aDoc.defaultView.gBrowser.tabContainer; - container.removeEventListener("TabSelect", updateZoomResetButton); - }.bind(this), - - onCustomizeStart: function(aWindow) { - if (aWindow.document == aDocument) { - updateZoomResetButton(); - } - }, - - onCustomizeEnd: function(aWindow) { - if (aWindow.document == aDocument) { - updateZoomResetButton(); - } - }, - - onWidgetDrag: function(aWidgetId, aArea) { - if (aWidgetId != this.id) - return; - aArea = aArea || this.currentArea; - updateCombinedWidgetStyle(node, aArea, true); - }.bind(this) - }; - CustomizableUI.addListener(listener); - - return node; - } - }, { - id: "edit-controls", - type: "custom", - tooltiptext: "edit-controls.tooltiptext2", - defaultArea: CustomizableUI.AREA_PANEL, - onBuild: function(aDocument) { - let buttons = [{ - id: "cut-button", - command: "cmd_cut", - label: true, - tooltiptext: "tooltiptext2", - shortcutId: "key_cut", - }, { - id: "copy-button", - command: "cmd_copy", - label: true, - tooltiptext: "tooltiptext2", - shortcutId: "key_copy", - }, { - id: "paste-button", - command: "cmd_paste", - label: true, - tooltiptext: "tooltiptext2", - shortcutId: "key_paste", - }]; - - let node = aDocument.createElementNS(kNSXUL, "toolbaritem"); - node.setAttribute("id", "edit-controls"); - node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label")); - node.setAttribute("title", CustomizableUI.getLocalizedProperty(this, "tooltiptext")); - // Set this as an attribute in addition to the property to make sure we can style correctly. - node.setAttribute("removable", "true"); - node.classList.add("chromeclass-toolbar-additional"); - node.classList.add("toolbaritem-combined-buttons"); - node.classList.add(kWidePanelItemClass); - - buttons.forEach(function(aButton, aIndex) { - if (aIndex != 0) - node.appendChild(aDocument.createElementNS(kNSXUL, "separator")); - let btnNode = aDocument.createElementNS(kNSXUL, "toolbarbutton"); - setAttributes(btnNode, aButton); - node.appendChild(btnNode); - }); - - updateCombinedWidgetStyle(node, this.currentArea); - - let listener = { - onWidgetAdded: function(aWidgetId, aArea, aPosition) { - if (aWidgetId != this.id) - return; - updateCombinedWidgetStyle(node, aArea); - }.bind(this), - - onWidgetRemoved: function(aWidgetId, aPrevArea) { - if (aWidgetId != this.id) - return; - // When a widget is demoted to the palette ('removed'), it's visual - // style should change. - updateCombinedWidgetStyle(node); - }.bind(this), - - onWidgetReset: function(aWidgetNode) { - if (aWidgetNode != node) - return; - updateCombinedWidgetStyle(node, this.currentArea); - }.bind(this), - - onWidgetUndoMove: function(aWidgetNode) { - if (aWidgetNode != node) - return; - updateCombinedWidgetStyle(node, this.currentArea); - }.bind(this), - - onWidgetMoved: function(aWidgetId, aArea) { - if (aWidgetId != this.id) - return; - updateCombinedWidgetStyle(node, aArea); - }.bind(this), - - onWidgetInstanceRemoved: function(aWidgetId, aDoc) { - if (aWidgetId != this.id || aDoc != aDocument) - return; - CustomizableUI.removeListener(listener); - }.bind(this), - - onWidgetDrag: function(aWidgetId, aArea) { - if (aWidgetId != this.id) - return; - aArea = aArea || this.currentArea; - updateCombinedWidgetStyle(node, aArea); - }.bind(this) - }; - CustomizableUI.addListener(listener); - - return node; - } - }, - { - id: "feed-button", - type: "view", - viewId: "PanelUI-feeds", - tooltiptext: "feed-button.tooltiptext2", - defaultArea: CustomizableUI.AREA_PANEL, - onClick: function(aEvent) { - let win = aEvent.target.ownerGlobal; - let feeds = win.gBrowser.selectedBrowser.feeds; - - // Here, we only care about the case where we have exactly 1 feed and the - // user clicked... - let isClick = (aEvent.button == 0 || aEvent.button == 1); - if (feeds && feeds.length == 1 && isClick) { - aEvent.preventDefault(); - aEvent.stopPropagation(); - win.FeedHandler.subscribeToFeed(feeds[0].href, aEvent); - CustomizableUI.hidePanelForNode(aEvent.target); - } - }, - onViewShowing: function(aEvent) { - let doc = aEvent.target.ownerDocument; - let container = doc.getElementById("PanelUI-feeds"); - let gotView = doc.defaultView.FeedHandler.buildFeedList(container, true); - - // For no feeds or only a single one, don't show the panel. - if (!gotView) { - aEvent.preventDefault(); - aEvent.stopPropagation(); - return; - } - }, - onCreated: function(node) { - let win = node.ownerGlobal; - let selectedBrowser = win.gBrowser.selectedBrowser; - let feeds = selectedBrowser && selectedBrowser.feeds; - if (!feeds || !feeds.length) { - node.setAttribute("disabled", "true"); - } - } - }, { - id: "characterencoding-button", - label: "characterencoding-button2.label", - type: "view", - viewId: "PanelUI-characterEncodingView", - tooltiptext: "characterencoding-button2.tooltiptext", - defaultArea: CustomizableUI.AREA_PANEL, - maybeDisableMenu: function(aDocument) { - let window = aDocument.defaultView; - return !(window.gBrowser && - window.gBrowser.selectedBrowser.mayEnableCharacterEncodingMenu); - }, - populateList: function(aDocument, aContainerId, aSection) { - let containerElem = aDocument.getElementById(aContainerId); - - containerElem.addEventListener("command", this.onCommand, false); - - let list = this.charsetInfo[aSection]; - - for (let item of list) { - let elem = aDocument.createElementNS(kNSXUL, "toolbarbutton"); - elem.setAttribute("label", item.label); - elem.setAttribute("type", "checkbox"); - elem.section = aSection; - elem.value = item.value; - elem.setAttribute("class", "subviewbutton"); - containerElem.appendChild(elem); - } - }, - updateCurrentCharset: function(aDocument) { - let currentCharset = aDocument.defaultView.gBrowser.selectedBrowser.characterSet; - currentCharset = CharsetMenu.foldCharset(currentCharset); - - let pinnedContainer = aDocument.getElementById("PanelUI-characterEncodingView-pinned"); - let charsetContainer = aDocument.getElementById("PanelUI-characterEncodingView-charsets"); - let elements = [...(pinnedContainer.childNodes), ...(charsetContainer.childNodes)]; - - this._updateElements(elements, currentCharset); - }, - updateCurrentDetector: function(aDocument) { - let detectorContainer = aDocument.getElementById("PanelUI-characterEncodingView-autodetect"); - let currentDetector; - try { - currentDetector = Services.prefs.getComplexValue( - "intl.charset.detector", Ci.nsIPrefLocalizedString).data; - } catch (e) {} - - this._updateElements(detectorContainer.childNodes, currentDetector); - }, - _updateElements: function(aElements, aCurrentItem) { - if (!aElements.length) { - return; - } - let disabled = this.maybeDisableMenu(aElements[0].ownerDocument); - for (let elem of aElements) { - if (disabled) { - elem.setAttribute("disabled", "true"); - } else { - elem.removeAttribute("disabled"); - } - if (elem.value.toLowerCase() == aCurrentItem.toLowerCase()) { - elem.setAttribute("checked", "true"); - } else { - elem.removeAttribute("checked"); - } - } - }, - onViewShowing: function(aEvent) { - let document = aEvent.target.ownerDocument; - - let autoDetectLabelId = "PanelUI-characterEncodingView-autodetect-label"; - let autoDetectLabel = document.getElementById(autoDetectLabelId); - if (!autoDetectLabel.hasAttribute("value")) { - let label = CharsetBundle.GetStringFromName("charsetMenuAutodet"); - autoDetectLabel.setAttribute("value", label); - this.populateList(document, - "PanelUI-characterEncodingView-pinned", - "pinnedCharsets"); - this.populateList(document, - "PanelUI-characterEncodingView-charsets", - "otherCharsets"); - this.populateList(document, - "PanelUI-characterEncodingView-autodetect", - "detectors"); - } - this.updateCurrentDetector(document); - this.updateCurrentCharset(document); - }, - onCommand: function(aEvent) { - let node = aEvent.target; - if (!node.hasAttribute || !node.section) { - return; - } - - let window = node.ownerGlobal; - let section = node.section; - let value = node.value; - - // The behavior as implemented here is directly based off of the - // `MultiplexHandler()` method in browser.js. - if (section != "detectors") { - window.BrowserSetForcedCharacterSet(value); - } else { - // Set the detector pref. - try { - let str = Cc["@mozilla.org/supports-string;1"] - .createInstance(Ci.nsISupportsString); - str.data = value; - Services.prefs.setComplexValue("intl.charset.detector", Ci.nsISupportsString, str); - } catch (e) { - Cu.reportError("Failed to set the intl.charset.detector preference."); - } - // Prepare a browser page reload with a changed charset. - window.BrowserCharsetReload(); - } - }, - onCreated: function(aNode) { - const kPanelId = "PanelUI-popup"; - let document = aNode.ownerDocument; - - let updateButton = () => { - if (this.maybeDisableMenu(document)) - aNode.setAttribute("disabled", "true"); - else - aNode.removeAttribute("disabled"); - }; - - if (this.currentArea == CustomizableUI.AREA_PANEL) { - let panel = document.getElementById(kPanelId); - panel.addEventListener("popupshowing", updateButton); - } - - let listener = { - onWidgetAdded: (aWidgetId, aArea) => { - if (aWidgetId != this.id) - return; - if (aArea == CustomizableUI.AREA_PANEL) { - let panel = document.getElementById(kPanelId); - panel.addEventListener("popupshowing", updateButton); - } - }, - onWidgetRemoved: (aWidgetId, aPrevArea) => { - if (aWidgetId != this.id) - return; - aNode.removeAttribute("disabled"); - if (aPrevArea == CustomizableUI.AREA_PANEL) { - let panel = document.getElementById(kPanelId); - panel.removeEventListener("popupshowing", updateButton); - } - }, - onWidgetInstanceRemoved: (aWidgetId, aDoc) => { - if (aWidgetId != this.id || aDoc != document) - return; - - CustomizableUI.removeListener(listener); - let panel = aDoc.getElementById(kPanelId); - panel.removeEventListener("popupshowing", updateButton); - } - }; - CustomizableUI.addListener(listener); - if (!this.charsetInfo) { - this.charsetInfo = CharsetMenu.getData(); - } - } - }, { - id: "email-link-button", - tooltiptext: "email-link-button.tooltiptext3", - onCommand: function(aEvent) { - let win = aEvent.view; - win.MailIntegration.sendLinkForBrowser(win.gBrowser.selectedBrowser) - } - }, { - id: "containers-panelmenu", - type: "view", - viewId: "PanelUI-containers", - hasObserver: false, - onCreated: function(aNode) { - let doc = aNode.ownerDocument; - let win = doc.defaultView; - let items = doc.getElementById("PanelUI-containersItems"); - - let onItemCommand = function (aEvent) { - let item = aEvent.target; - if (item.hasAttribute("usercontextid")) { - let userContextId = parseInt(item.getAttribute("usercontextid")); - win.openUILinkIn(win.BROWSER_NEW_TAB_URL, "tab", {userContextId}); - } - }; - items.addEventListener("command", onItemCommand); - - if (PrivateBrowsingUtils.isWindowPrivate(win)) { - aNode.setAttribute("disabled", "true"); - } - - this.updateVisibility(aNode); - - if (!this.hasObserver) { - Services.prefs.addObserver("privacy.userContext.enabled", this, true); - this.hasObserver = true; - } - }, - onViewShowing: function(aEvent) { - let doc = aEvent.target.ownerDocument; - - let items = doc.getElementById("PanelUI-containersItems"); - - while (items.firstChild) { - items.firstChild.remove(); - } - - let fragment = doc.createDocumentFragment(); - let bundle = doc.getElementById("bundle_browser"); - - ContextualIdentityService.getIdentities().forEach(identity => { - let label = ContextualIdentityService.getUserContextLabel(identity.userContextId); - - let item = doc.createElementNS(kNSXUL, "toolbarbutton"); - item.setAttribute("label", label); - item.setAttribute("usercontextid", identity.userContextId); - item.setAttribute("class", "subviewbutton"); - item.setAttribute("data-identity-color", identity.color); - item.setAttribute("data-identity-icon", identity.icon); - - fragment.appendChild(item); - }); - - fragment.appendChild(doc.createElementNS(kNSXUL, "menuseparator")); - - let item = doc.createElementNS(kNSXUL, "toolbarbutton"); - item.setAttribute("label", bundle.getString("userContext.aboutPage.label")); - item.setAttribute("command", "Browser:OpenAboutContainers"); - item.setAttribute("class", "subviewbutton"); - fragment.appendChild(item); - - items.appendChild(fragment); - }, - - updateVisibility(aNode) { - aNode.hidden = !Services.prefs.getBoolPref("privacy.userContext.enabled"); - }, - - observe(aSubject, aTopic, aData) { - let {instances} = CustomizableUI.getWidget("containers-panelmenu"); - for (let {node} of instances) { - if (node) { - this.updateVisibility(node); - } - } - }, - - QueryInterface: XPCOMUtils.generateQI([ - Ci.nsISupportsWeakReference, - Ci.nsIObserver - ]), - }]; - -let preferencesButton = { - id: "preferences-button", - defaultArea: CustomizableUI.AREA_PANEL, - onCommand: function(aEvent) { - let win = aEvent.target.ownerGlobal; - win.openPreferences(); - } -}; -if (AppConstants.platform == "macosx") { - preferencesButton.tooltiptext = "preferences-button.tooltiptext.withshortcut"; - preferencesButton.shortcutId = "key_preferencesCmdMac"; -} else { - preferencesButton.tooltiptext = "preferences-button.tooltiptext2"; -} -CustomizableWidgets.push(preferencesButton); - -if (Services.prefs.getBoolPref("privacy.panicButton.enabled")) { - CustomizableWidgets.push({ - id: "panic-button", - type: "view", - viewId: "PanelUI-panicView", - _sanitizer: null, - _ensureSanitizer: function() { - if (!this.sanitizer) { - let scope = {}; - Services.scriptloader.loadSubScript("chrome://browser/content/sanitize.js", - scope); - this._Sanitizer = scope.Sanitizer; - this._sanitizer = new scope.Sanitizer(); - this._sanitizer.ignoreTimespan = false; - } - }, - _getSanitizeRange: function(aDocument) { - let group = aDocument.getElementById("PanelUI-panic-timeSpan"); - return this._Sanitizer.getClearRange(+group.value); - }, - forgetButtonCalled: function(aEvent) { - let doc = aEvent.target.ownerDocument; - this._ensureSanitizer(); - this._sanitizer.range = this._getSanitizeRange(doc); - let group = doc.getElementById("PanelUI-panic-timeSpan"); - BrowserUITelemetry.countPanicEvent(group.selectedItem.id); - group.selectedItem = doc.getElementById("PanelUI-panic-5min"); - let itemsToClear = [ - "cookies", "history", "openWindows", "formdata", "sessions", "cache", "downloads" - ]; - let newWindowPrivateState = PrivateBrowsingUtils.isWindowPrivate(doc.defaultView) ? - "private" : "non-private"; - this._sanitizer.items.openWindows.privateStateForNewWindow = newWindowPrivateState; - let promise = this._sanitizer.sanitize(itemsToClear); - promise.then(function() { - let otherWindow = Services.wm.getMostRecentWindow("navigator:browser"); - if (otherWindow.closed) { - Cu.reportError("Got a closed window!"); - } - if (otherWindow.PanicButtonNotifier) { - otherWindow.PanicButtonNotifier.notify(); - } else { - otherWindow.PanicButtonNotifierShouldNotify = true; - } - }); - }, - handleEvent: function(aEvent) { - switch (aEvent.type) { - case "command": - this.forgetButtonCalled(aEvent); - break; - } - }, - onViewShowing: function(aEvent) { - let forgetButton = aEvent.target.querySelector("#PanelUI-panic-view-button"); - forgetButton.addEventListener("command", this); - }, - onViewHiding: function(aEvent) { - let forgetButton = aEvent.target.querySelector("#PanelUI-panic-view-button"); - forgetButton.removeEventListener("command", this); - }, - }); -} diff --git a/browser/components/customizableui/CustomizeMode.jsm b/browser/components/customizableui/CustomizeMode.jsm deleted file mode 100644 index e63e25b0a..000000000 --- a/browser/components/customizableui/CustomizeMode.jsm +++ /dev/null @@ -1,2297 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["CustomizeMode"]; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -const kPrefCustomizationDebug = "browser.uiCustomization.debug"; -const kPrefCustomizationAnimation = "browser.uiCustomization.disableAnimation"; -const kPaletteId = "customization-palette"; -const kDragDataTypePrefix = "text/toolbarwrapper-id/"; -const kPlaceholderClass = "panel-customization-placeholder"; -const kSkipSourceNodePref = "browser.uiCustomization.skipSourceNodeCheck"; -const kToolbarVisibilityBtn = "customization-toolbar-visibility-button"; -const kDrawInTitlebarPref = "browser.tabs.drawInTitlebar"; -const kMaxTransitionDurationMs = 2000; - -const kPanelItemContextMenu = "customizationPanelItemContextMenu"; -const kPaletteItemContextMenu = "customizationPaletteItemContextMenu"; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource:///modules/CustomizableUI.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -Cu.import("resource://gre/modules/Task.jsm"); -Cu.import("resource://gre/modules/Promise.jsm"); -Cu.import("resource://gre/modules/AddonManager.jsm"); -Cu.import("resource://gre/modules/AppConstants.jsm"); - -XPCOMUtils.defineLazyModuleGetter(this, "DragPositionManager", - "resource:///modules/DragPositionManager.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "BrowserUITelemetry", - "resource:///modules/BrowserUITelemetry.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "LightweightThemeManager", - "resource://gre/modules/LightweightThemeManager.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "SessionStore", - "resource:///modules/sessionstore/SessionStore.jsm"); - -let gDebug; -XPCOMUtils.defineLazyGetter(this, "log", () => { - let scope = {}; - Cu.import("resource://gre/modules/Console.jsm", scope); - try { - gDebug = Services.prefs.getBoolPref(kPrefCustomizationDebug); - } catch (ex) {} - let consoleOptions = { - maxLogLevel: gDebug ? "all" : "log", - prefix: "CustomizeMode", - }; - return new scope.ConsoleAPI(consoleOptions); -}); - -var gDisableAnimation = null; - -var gDraggingInToolbars; - -var gTab; - -function closeGlobalTab() { - let win = gTab.ownerGlobal; - if (win.gBrowser.browsers.length == 1) { - win.BrowserOpenTab(); - } - win.gBrowser.removeTab(gTab); - gTab = null; -} - -function unregisterGlobalTab() { - gTab.removeEventListener("TabClose", unregisterGlobalTab); - gTab.ownerGlobal.removeEventListener("unload", unregisterGlobalTab); - gTab.removeAttribute("customizemode"); - gTab = null; -} - -function CustomizeMode(aWindow) { - if (gDisableAnimation === null) { - gDisableAnimation = Services.prefs.getPrefType(kPrefCustomizationAnimation) == Ci.nsIPrefBranch.PREF_BOOL && - Services.prefs.getBoolPref(kPrefCustomizationAnimation); - } - this.window = aWindow; - this.document = aWindow.document; - this.browser = aWindow.gBrowser; - this.areas = new Set(); - - // There are two palettes - there's the palette that can be overlayed with - // toolbar items in browser.xul. This is invisible, and never seen by the - // user. Then there's the visible palette, which gets populated and displayed - // to the user when in customizing mode. - this.visiblePalette = this.document.getElementById(kPaletteId); - this.paletteEmptyNotice = this.document.getElementById("customization-empty"); - if (Services.prefs.getCharPref("general.skins.selectedSkin") != "classic/1.0") { - let lwthemeButton = this.document.getElementById("customization-lwtheme-button"); - lwthemeButton.setAttribute("hidden", "true"); - } - if (AppConstants.CAN_DRAW_IN_TITLEBAR) { - this._updateTitlebarButton(); - Services.prefs.addObserver(kDrawInTitlebarPref, this, false); - } - this.window.addEventListener("unload", this); -} - -CustomizeMode.prototype = { - _changed: false, - _transitioning: false, - window: null, - document: null, - // areas is used to cache the customizable areas when in customization mode. - areas: null, - // When in customizing mode, we swap out the reference to the invisible - // palette in gNavToolbox.palette for our visiblePalette. This way, for the - // customizing browser window, when widgets are removed from customizable - // areas and added to the palette, they're added to the visible palette. - // _stowedPalette is a reference to the old invisible palette so we can - // restore gNavToolbox.palette to its original state after exiting - // customization mode. - _stowedPalette: null, - _dragOverItem: null, - _customizing: false, - _skipSourceNodeCheck: null, - _mainViewContext: null, - - get panelUIContents() { - return this.document.getElementById("PanelUI-contents"); - }, - - get _handler() { - return this.window.CustomizationHandler; - }, - - uninit: function() { - if (AppConstants.CAN_DRAW_IN_TITLEBAR) { - Services.prefs.removeObserver(kDrawInTitlebarPref, this); - } - }, - - toggle: function() { - if (this._handler.isEnteringCustomizeMode || this._handler.isExitingCustomizeMode) { - this._wantToBeInCustomizeMode = !this._wantToBeInCustomizeMode; - return; - } - if (this._customizing) { - this.exit(); - } else { - this.enter(); - } - }, - - _updateLWThemeButtonIcon: function() { - let lwthemeButton = this.document.getElementById("customization-lwtheme-button"); - let lwthemeIcon = this.document.getAnonymousElementByAttribute(lwthemeButton, - "class", "button-icon"); - lwthemeIcon.style.backgroundImage = LightweightThemeManager.currentTheme ? - "url(" + LightweightThemeManager.currentTheme.iconURL + ")" : ""; - }, - - setTab: function(aTab) { - if (gTab == aTab) { - return; - } - - if (gTab) { - closeGlobalTab(); - } - - gTab = aTab; - - gTab.setAttribute("customizemode", "true"); - SessionStore.persistTabAttribute("customizemode"); - - gTab.linkedBrowser.stop(); - - let win = gTab.ownerGlobal; - - win.gBrowser.setTabTitle(gTab); - win.gBrowser.setIcon(gTab, - "chrome://browser/skin/customizableui/customizeFavicon.ico"); - - gTab.addEventListener("TabClose", unregisterGlobalTab); - win.addEventListener("unload", unregisterGlobalTab); - - if (gTab.selected) { - win.gCustomizeMode.enter(); - } - }, - - enter: function() { - this._wantToBeInCustomizeMode = true; - - if (this._customizing || this._handler.isEnteringCustomizeMode) { - return; - } - - // Exiting; want to re-enter once we've done that. - if (this._handler.isExitingCustomizeMode) { - log.debug("Attempted to enter while we're in the middle of exiting. " + - "We'll exit after we've entered"); - return; - } - - if (!gTab) { - this.setTab(this.browser.loadOneTab("about:blank", - { inBackground: false, - forceNotRemote: true, - skipAnimation: true })); - return; - } - if (!gTab.selected) { - // This will force another .enter() to be called via the - // onlocationchange handler of the tabbrowser, so we return early. - gTab.ownerGlobal.gBrowser.selectedTab = gTab; - return; - } - gTab.ownerGlobal.focus(); - if (gTab.ownerDocument != this.document) { - return; - } - - let window = this.window; - let document = this.document; - - this._handler.isEnteringCustomizeMode = true; - - // Always disable the reset button at the start of customize mode, it'll be re-enabled - // if necessary when we finish entering: - let resetButton = this.document.getElementById("customization-reset-button"); - resetButton.setAttribute("disabled", "true"); - - Task.spawn(function*() { - // We shouldn't start customize mode until after browser-delayed-startup has finished: - if (!this.window.gBrowserInit.delayedStartupFinished) { - yield new Promise(resolve => { - let delayedStartupObserver = aSubject => { - if (aSubject == this.window) { - Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished"); - resolve(); - } - }; - - Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false); - }); - } - - let toolbarVisibilityBtn = document.getElementById(kToolbarVisibilityBtn); - let togglableToolbars = window.getTogglableToolbars(); - if (togglableToolbars.length == 0) { - toolbarVisibilityBtn.setAttribute("hidden", "true"); - } else { - toolbarVisibilityBtn.removeAttribute("hidden"); - } - - this.updateLWTStyling(); - - CustomizableUI.dispatchToolboxEvent("beforecustomization", {}, window); - CustomizableUI.notifyStartCustomizing(this.window); - - // Add a keypress listener to the document so that we can quickly exit - // customization mode when pressing ESC. - document.addEventListener("keypress", this); - - // Same goes for the menu button - if we're customizing, a click on the - // menu button means a quick exit from customization mode. - window.PanelUI.hide(); - window.PanelUI.menuButton.addEventListener("command", this); - window.PanelUI.menuButton.open = true; - window.PanelUI.beginBatchUpdate(); - - // The menu panel is lazy, and registers itself when the popup shows. We - // need to force the menu panel to register itself, or else customization - // is really not going to work. We pass "true" to ensureReady to - // indicate that we're handling calling startBatchUpdate and - // endBatchUpdate. - if (!window.PanelUI.isReady) { - yield window.PanelUI.ensureReady(true); - } - - // Hide the palette before starting the transition for increased perf. - this.visiblePalette.hidden = true; - this.visiblePalette.removeAttribute("showing"); - - // Disable the button-text fade-out mask - // during the transition for increased perf. - let panelContents = window.PanelUI.contents; - panelContents.setAttribute("customize-transitioning", "true"); - - // Move the mainView in the panel to the holder so that we can see it - // while customizing. - let mainView = window.PanelUI.mainView; - let panelHolder = document.getElementById("customization-panelHolder"); - panelHolder.appendChild(mainView); - - let customizeButton = document.getElementById("PanelUI-customize"); - customizeButton.setAttribute("enterLabel", customizeButton.getAttribute("label")); - customizeButton.setAttribute("label", customizeButton.getAttribute("exitLabel")); - customizeButton.setAttribute("enterTooltiptext", customizeButton.getAttribute("tooltiptext")); - customizeButton.setAttribute("tooltiptext", customizeButton.getAttribute("exitTooltiptext")); - - this._transitioning = true; - - let customizer = document.getElementById("customization-container"); - customizer.parentNode.selectedPanel = customizer; - customizer.hidden = false; - - this._wrapToolbarItemSync(CustomizableUI.AREA_TABSTRIP); - - let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true]):not([collapsed=true])"); - for (let toolbar of customizableToolbars) - toolbar.setAttribute("customizing", true); - - yield this._doTransition(true); - - Services.obs.addObserver(this, "lightweight-theme-window-updated", false); - - // Let everybody in this window know that we're about to customize. - CustomizableUI.dispatchToolboxEvent("customizationstarting", {}, window); - - this._mainViewContext = mainView.getAttribute("context"); - if (this._mainViewContext) { - mainView.removeAttribute("context"); - } - - this._showPanelCustomizationPlaceholders(); - - yield this._wrapToolbarItems(); - this.populatePalette(); - - this._addDragHandlers(this.visiblePalette); - - window.gNavToolbox.addEventListener("toolbarvisibilitychange", this); - - document.getElementById("PanelUI-help").setAttribute("disabled", true); - document.getElementById("PanelUI-quit").setAttribute("disabled", true); - - this._updateResetButton(); - this._updateUndoResetButton(); - - this._skipSourceNodeCheck = Services.prefs.getPrefType(kSkipSourceNodePref) == Ci.nsIPrefBranch.PREF_BOOL && - Services.prefs.getBoolPref(kSkipSourceNodePref); - - CustomizableUI.addListener(this); - window.PanelUI.endBatchUpdate(); - this._customizing = true; - this._transitioning = false; - - // Show the palette now that the transition has finished. - this.visiblePalette.hidden = false; - window.setTimeout(() => { - // Force layout reflow to ensure the animation runs, - // and make it async so it doesn't affect the timing. - this.visiblePalette.clientTop; - this.visiblePalette.setAttribute("showing", "true"); - }, 0); - this._updateEmptyPaletteNotice(); - - this._updateLWThemeButtonIcon(); - - this._handler.isEnteringCustomizeMode = false; - panelContents.removeAttribute("customize-transitioning"); - - CustomizableUI.dispatchToolboxEvent("customizationready", {}, window); - this._enableOutlinesTimeout = window.setTimeout(() => { - this.document.getElementById("nav-bar").setAttribute("showoutline", "true"); - this.panelUIContents.setAttribute("showoutline", "true"); - delete this._enableOutlinesTimeout; - }, 0); - - if (!this._wantToBeInCustomizeMode) { - this.exit(); - } - }.bind(this)).then(null, function(e) { - log.error("Error entering customize mode", e); - // We should ensure this has been called, and calling it again doesn't hurt: - window.PanelUI.endBatchUpdate(); - this._handler.isEnteringCustomizeMode = false; - // Exit customize mode to ensure proper clean-up when entering failed. - this.exit(); - }.bind(this)); - }, - - exit: function() { - this._wantToBeInCustomizeMode = false; - - if (!this._customizing || this._handler.isExitingCustomizeMode) { - return; - } - - // Entering; want to exit once we've done that. - if (this._handler.isEnteringCustomizeMode) { - log.debug("Attempted to exit while we're in the middle of entering. " + - "We'll exit after we've entered"); - return; - } - - if (this.resetting) { - log.debug("Attempted to exit while we're resetting. " + - "We'll exit after resetting has finished."); - return; - } - - this._handler.isExitingCustomizeMode = true; - - if (this._enableOutlinesTimeout) { - this.window.clearTimeout(this._enableOutlinesTimeout); - } else { - this.document.getElementById("nav-bar").removeAttribute("showoutline"); - this.panelUIContents.removeAttribute("showoutline"); - } - - this._removeExtraToolbarsIfEmpty(); - - CustomizableUI.removeListener(this); - - this.document.removeEventListener("keypress", this); - this.window.PanelUI.menuButton.removeEventListener("command", this); - this.window.PanelUI.menuButton.open = false; - - this.window.PanelUI.beginBatchUpdate(); - - this._removePanelCustomizationPlaceholders(); - - let window = this.window; - let document = this.document; - - // Hide the palette before starting the transition for increased perf. - this.visiblePalette.hidden = true; - this.visiblePalette.removeAttribute("showing"); - this.paletteEmptyNotice.hidden = true; - - // Disable the button-text fade-out mask - // during the transition for increased perf. - let panelContents = window.PanelUI.contents; - panelContents.setAttribute("customize-transitioning", "true"); - - // Disable the reset and undo reset buttons while transitioning: - let resetButton = this.document.getElementById("customization-reset-button"); - let undoResetButton = this.document.getElementById("customization-undo-reset-button"); - undoResetButton.hidden = resetButton.disabled = true; - - this._transitioning = true; - - Task.spawn(function*() { - yield this.depopulatePalette(); - - yield this._doTransition(false); - this.removeLWTStyling(); - - Services.obs.removeObserver(this, "lightweight-theme-window-updated", false); - - if (this.browser.selectedTab == gTab) { - if (gTab.linkedBrowser.currentURI.spec == "about:blank") { - closeGlobalTab(); - } else { - unregisterGlobalTab(); - } - } - let browser = document.getElementById("browser"); - browser.parentNode.selectedPanel = browser; - let customizer = document.getElementById("customization-container"); - customizer.hidden = true; - - window.gNavToolbox.removeEventListener("toolbarvisibilitychange", this); - - DragPositionManager.stop(); - this._removeDragHandlers(this.visiblePalette); - - yield this._unwrapToolbarItems(); - - if (this._changed) { - // XXXmconley: At first, it seems strange to also persist the old way with - // currentset - but this might actually be useful for switching - // to old builds. We might want to keep this around for a little - // bit. - this.persistCurrentSets(); - } - - // And drop all area references. - this.areas.clear(); - - // Let everybody in this window know that we're starting to - // exit customization mode. - CustomizableUI.dispatchToolboxEvent("customizationending", {}, window); - - window.PanelUI.setMainView(window.PanelUI.mainView); - window.PanelUI.menuButton.disabled = false; - - let customizeButton = document.getElementById("PanelUI-customize"); - customizeButton.setAttribute("exitLabel", customizeButton.getAttribute("label")); - customizeButton.setAttribute("label", customizeButton.getAttribute("enterLabel")); - customizeButton.setAttribute("exitTooltiptext", customizeButton.getAttribute("tooltiptext")); - customizeButton.setAttribute("tooltiptext", customizeButton.getAttribute("enterTooltiptext")); - - // We have to use setAttribute/removeAttribute here instead of the - // property because the XBL property will be set later, and right - // now we'd be setting an expando, which breaks the XBL property. - document.getElementById("PanelUI-help").removeAttribute("disabled"); - document.getElementById("PanelUI-quit").removeAttribute("disabled"); - - panelContents.removeAttribute("customize-transitioning"); - - // We need to set this._customizing to false before removing the tab - // or the TabSelect event handler will think that we are exiting - // customization mode for a second time. - this._customizing = false; - - let mainView = window.PanelUI.mainView; - if (this._mainViewContext) { - mainView.setAttribute("context", this._mainViewContext); - } - - let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true])"); - for (let toolbar of customizableToolbars) - toolbar.removeAttribute("customizing"); - - this.window.PanelUI.endBatchUpdate(); - delete this._lastLightweightTheme; - this._changed = false; - this._transitioning = false; - this._handler.isExitingCustomizeMode = false; - CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, window); - CustomizableUI.notifyEndCustomizing(window); - - if (this._wantToBeInCustomizeMode) { - this.enter(); - } - }.bind(this)).then(null, function(e) { - log.error("Error exiting customize mode", e); - // We should ensure this has been called, and calling it again doesn't hurt: - window.PanelUI.endBatchUpdate(); - this._handler.isExitingCustomizeMode = false; - }.bind(this)); - }, - - /** - * The customize mode transition has 4 phases when entering: - * 1) Pre-customization mode - * This is the starting phase of the browser. - * 2) LWT swapping - * This is where we swap some of the lightweight theme styles in order - * to make them work in customize mode. We set/unset a customization- - * lwtheme attribute iff we're using a lightweight theme. - * 3) customize-entering - * This phase is a transition, optimized for smoothness. - * 4) customize-entered - * After the transition completes, this phase draws all of the - * expensive detail that isn't necessary during the second phase. - * - * Exiting customization mode has a similar set of phases, but in reverse - * order - customize-entered, customize-exiting, remove LWT swapping, - * pre-customization mode. - * - * When in the customize-entering, customize-entered, or customize-exiting - * phases, there is a "customizing" attribute set on the main-window to simplify - * excluding certain styles while in any phase of customize mode. - */ - _doTransition: function(aEntering) { - let deck = this.document.getElementById("content-deck"); - let customizeTransitionEndPromise = new Promise(resolve => { - let customizeTransitionEnd = (aEvent) => { - if (aEvent != "timedout" && - (aEvent.originalTarget != deck || aEvent.propertyName != "margin-left")) { - return; - } - this.window.clearTimeout(catchAllTimeout); - // We request an animation frame to do the final stage of the transition - // to improve perceived performance. (bug 962677) - this.window.requestAnimationFrame(() => { - deck.removeEventListener("transitionend", customizeTransitionEnd); - - if (!aEntering) { - this.document.documentElement.removeAttribute("customize-exiting"); - this.document.documentElement.removeAttribute("customizing"); - } else { - this.document.documentElement.setAttribute("customize-entered", true); - this.document.documentElement.removeAttribute("customize-entering"); - } - CustomizableUI.dispatchToolboxEvent("customization-transitionend", aEntering, this.window); - - resolve(); - }); - }; - deck.addEventListener("transitionend", customizeTransitionEnd); - let catchAll = () => customizeTransitionEnd("timedout"); - let catchAllTimeout = this.window.setTimeout(catchAll, kMaxTransitionDurationMs); - }); - - if (gDisableAnimation) { - this.document.getElementById("tab-view-deck").setAttribute("fastcustomizeanimation", true); - } - - if (aEntering) { - this.document.documentElement.setAttribute("customizing", true); - this.document.documentElement.setAttribute("customize-entering", true); - } else { - this.document.documentElement.setAttribute("customize-exiting", true); - this.document.documentElement.removeAttribute("customize-entered"); - } - - return customizeTransitionEndPromise; - }, - - updateLWTStyling: function(aData) { - let docElement = this.document.documentElement; - if (!aData) { - let lwt = docElement._lightweightTheme; - aData = lwt.getData(); - } - let headerURL = aData && aData.headerURL; - if (!headerURL) { - this.removeLWTStyling(); - return; - } - - let deck = this.document.getElementById("tab-view-deck"); - let headerImageRef = this._getHeaderImageRef(aData); - docElement.setAttribute("customization-lwtheme", "true"); - - let toolboxRect = this.window.gNavToolbox.getBoundingClientRect(); - let height = toolboxRect.bottom; - - if (AppConstants.platform == "macosx") { - let drawingInTitlebar = !docElement.hasAttribute("drawtitle"); - let titlebar = this.document.getElementById("titlebar"); - if (drawingInTitlebar) { - titlebar.style.backgroundImage = headerImageRef; - } else { - titlebar.style.removeProperty("background-image"); - } - } - - let limitedBG = "-moz-image-rect(" + headerImageRef + ", 0, 100%, " + - height + ", 0)"; - - let ridgeStart = height - 1; - let ridgeCenter = (ridgeStart + 1) + "px"; - let ridgeEnd = (ridgeStart + 2) + "px"; - ridgeStart = ridgeStart + "px"; - - let ridge = "linear-gradient(to bottom, " + - "transparent " + ridgeStart + - ", rgba(0,0,0,0.25) " + ridgeStart + - ", rgba(0,0,0,0.25) " + ridgeCenter + - ", rgba(255,255,255,0.5) " + ridgeCenter + - ", rgba(255,255,255,0.5) " + ridgeEnd + ", " + - "transparent " + ridgeEnd + ")"; - deck.style.backgroundImage = ridge + ", " + limitedBG; - - /* Remove the background styles from the <window> so we can style it instead. */ - docElement.style.removeProperty("background-image"); - docElement.style.removeProperty("background-color"); - }, - - removeLWTStyling: function() { - let affectedNodes = AppConstants.platform == "macosx" ? - ["tab-view-deck", "titlebar"] : - ["tab-view-deck"]; - for (let id of affectedNodes) { - let node = this.document.getElementById(id); - node.style.removeProperty("background-image"); - } - let docElement = this.document.documentElement; - docElement.removeAttribute("customization-lwtheme"); - let data = docElement._lightweightTheme.getData(); - if (data && data.headerURL) { - docElement.style.backgroundImage = this._getHeaderImageRef(data); - docElement.style.backgroundColor = data.accentcolor || "white"; - } - }, - - _getHeaderImageRef: function(aData) { - return "url(\"" + aData.headerURL.replace(/"/g, '\\"') + "\")"; - }, - - _getCustomizableChildForNode: function(aNode) { - // NB: adjusted from _getCustomizableParent to keep that method fast - // (it's used during drags), and avoid multiple DOM loops - let areas = CustomizableUI.areas; - // Caching this length is important because otherwise we'll also iterate - // over items we add to the end from within the loop. - let numberOfAreas = areas.length; - for (let i = 0; i < numberOfAreas; i++) { - let area = areas[i]; - let areaNode = aNode.ownerDocument.getElementById(area); - let customizationTarget = areaNode && areaNode.customizationTarget; - if (customizationTarget && customizationTarget != areaNode) { - areas.push(customizationTarget.id); - } - let overflowTarget = areaNode && areaNode.getAttribute("overflowtarget"); - if (overflowTarget) { - areas.push(overflowTarget); - } - } - areas.push(kPaletteId); - - while (aNode && aNode.parentNode) { - let parent = aNode.parentNode; - if (areas.indexOf(parent.id) != -1) { - return aNode; - } - aNode = parent; - } - return null; - }, - - addToToolbar: function(aNode) { - aNode = this._getCustomizableChildForNode(aNode); - if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) { - aNode = aNode.firstChild; - } - CustomizableUI.addWidgetToArea(aNode.id, CustomizableUI.AREA_NAVBAR); - if (!this._customizing) { - CustomizableUI.dispatchToolboxEvent("customizationchange"); - } - }, - - addToPanel: function(aNode) { - aNode = this._getCustomizableChildForNode(aNode); - if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) { - aNode = aNode.firstChild; - } - CustomizableUI.addWidgetToArea(aNode.id, CustomizableUI.AREA_PANEL); - if (!this._customizing) { - CustomizableUI.dispatchToolboxEvent("customizationchange"); - } - }, - - removeFromArea: function(aNode) { - aNode = this._getCustomizableChildForNode(aNode); - if (aNode.localName == "toolbarpaletteitem" && aNode.firstChild) { - aNode = aNode.firstChild; - } - CustomizableUI.removeWidgetFromArea(aNode.id); - if (!this._customizing) { - CustomizableUI.dispatchToolboxEvent("customizationchange"); - } - }, - - populatePalette: function() { - let fragment = this.document.createDocumentFragment(); - let toolboxPalette = this.window.gNavToolbox.palette; - - try { - let unusedWidgets = CustomizableUI.getUnusedWidgets(toolboxPalette); - for (let widget of unusedWidgets) { - let paletteItem = this.makePaletteItem(widget, "palette"); - if (!paletteItem) { - continue; - } - fragment.appendChild(paletteItem); - } - - this.visiblePalette.appendChild(fragment); - this._stowedPalette = this.window.gNavToolbox.palette; - this.window.gNavToolbox.palette = this.visiblePalette; - } catch (ex) { - log.error(ex); - } - }, - - // XXXunf Maybe this should use -moz-element instead of wrapping the node? - // Would ensure no weird interactions/event handling from original node, - // and makes it possible to put this in a lazy-loaded iframe/real tab - // while still getting rid of the need for overlays. - makePaletteItem: function(aWidget, aPlace) { - let widgetNode = aWidget.forWindow(this.window).node; - if (!widgetNode) { - log.error("Widget with id " + aWidget.id + " does not return a valid node"); - return null; - } - // Do not build a palette item for hidden widgets; there's not much to show. - if (widgetNode.hidden) { - return null; - } - - let wrapper = this.createOrUpdateWrapper(widgetNode, aPlace); - wrapper.appendChild(widgetNode); - return wrapper; - }, - - depopulatePalette: function() { - return Task.spawn(function*() { - this.visiblePalette.hidden = true; - let paletteChild = this.visiblePalette.firstChild; - let nextChild; - while (paletteChild) { - nextChild = paletteChild.nextElementSibling; - let provider = CustomizableUI.getWidget(paletteChild.id).provider; - if (provider == CustomizableUI.PROVIDER_XUL) { - let unwrappedPaletteItem = - yield this.deferredUnwrapToolbarItem(paletteChild); - this._stowedPalette.appendChild(unwrappedPaletteItem); - } else if (provider == CustomizableUI.PROVIDER_API) { - // XXXunf Currently this doesn't destroy the (now unused) node. It would - // be good to do so, but we need to keep strong refs to it in - // CustomizableUI (can't iterate of WeakMaps), and there's the - // question of what behavior wrappers should have if consumers - // keep hold of them. - // widget.destroyInstance(widgetNode); - } else if (provider == CustomizableUI.PROVIDER_SPECIAL) { - this.visiblePalette.removeChild(paletteChild); - } - - paletteChild = nextChild; - } - this.visiblePalette.hidden = false; - this.window.gNavToolbox.palette = this._stowedPalette; - }.bind(this)).then(null, log.error); - }, - - isCustomizableItem: function(aNode) { - return aNode.localName == "toolbarbutton" || - aNode.localName == "toolbaritem" || - aNode.localName == "toolbarseparator" || - aNode.localName == "toolbarspring" || - aNode.localName == "toolbarspacer"; - }, - - isWrappedToolbarItem: function(aNode) { - return aNode.localName == "toolbarpaletteitem"; - }, - - deferredWrapToolbarItem: function(aNode, aPlace) { - return new Promise(resolve => { - dispatchFunction(() => { - let wrapper = this.wrapToolbarItem(aNode, aPlace); - resolve(wrapper); - }); - }); - }, - - wrapToolbarItem: function(aNode, aPlace) { - if (!this.isCustomizableItem(aNode)) { - return aNode; - } - let wrapper = this.createOrUpdateWrapper(aNode, aPlace); - - // It's possible that this toolbar node is "mid-flight" and doesn't have - // a parent, in which case we skip replacing it. This can happen if a - // toolbar item has been dragged into the palette. In that case, we tell - // CustomizableUI to remove the widget from its area before putting the - // widget in the palette - so the node will have no parent. - if (aNode.parentNode) { - aNode = aNode.parentNode.replaceChild(wrapper, aNode); - } - wrapper.appendChild(aNode); - return wrapper; - }, - - createOrUpdateWrapper: function(aNode, aPlace, aIsUpdate) { - let wrapper; - if (aIsUpdate && aNode.parentNode && aNode.parentNode.localName == "toolbarpaletteitem") { - wrapper = aNode.parentNode; - aPlace = wrapper.getAttribute("place"); - } else { - wrapper = this.document.createElement("toolbarpaletteitem"); - // "place" is used by toolkit to add the toolbarpaletteitem-palette - // binding to a toolbarpaletteitem, which gives it a label node for when - // it's sitting in the palette. - wrapper.setAttribute("place", aPlace); - } - - - // Ensure the wrapped item doesn't look like it's in any special state, and - // can't be interactved with when in the customization palette. - if (aNode.hasAttribute("command")) { - wrapper.setAttribute("itemcommand", aNode.getAttribute("command")); - aNode.removeAttribute("command"); - } - - if (aNode.hasAttribute("observes")) { - wrapper.setAttribute("itemobserves", aNode.getAttribute("observes")); - aNode.removeAttribute("observes"); - } - - if (aNode.getAttribute("checked") == "true") { - wrapper.setAttribute("itemchecked", "true"); - aNode.removeAttribute("checked"); - } - - if (aNode.hasAttribute("id")) { - wrapper.setAttribute("id", "wrapper-" + aNode.getAttribute("id")); - } - - if (aNode.hasAttribute("label")) { - wrapper.setAttribute("title", aNode.getAttribute("label")); - wrapper.setAttribute("tooltiptext", aNode.getAttribute("label")); - } else if (aNode.hasAttribute("title")) { - wrapper.setAttribute("title", aNode.getAttribute("title")); - wrapper.setAttribute("tooltiptext", aNode.getAttribute("title")); - } - - if (aNode.hasAttribute("flex")) { - wrapper.setAttribute("flex", aNode.getAttribute("flex")); - } - - if (aPlace == "panel") { - if (aNode.classList.contains(CustomizableUI.WIDE_PANEL_CLASS)) { - wrapper.setAttribute("haswideitem", "true"); - } else if (wrapper.hasAttribute("haswideitem")) { - wrapper.removeAttribute("haswideitem"); - } - } - - let removable = aPlace == "palette" || CustomizableUI.isWidgetRemovable(aNode); - wrapper.setAttribute("removable", removable); - - let contextMenuAttrName = ""; - if (aNode.getAttribute("context")) { - contextMenuAttrName = "context"; - } else if (aNode.getAttribute("contextmenu")) { - contextMenuAttrName = "contextmenu"; - } - let currentContextMenu = aNode.getAttribute(contextMenuAttrName); - let contextMenuForPlace = aPlace == "panel" ? - kPanelItemContextMenu : - kPaletteItemContextMenu; - if (aPlace != "toolbar") { - wrapper.setAttribute("context", contextMenuForPlace); - } - // Only keep track of the menu if it is non-default. - if (currentContextMenu && - currentContextMenu != contextMenuForPlace) { - aNode.setAttribute("wrapped-context", currentContextMenu); - aNode.setAttribute("wrapped-contextAttrName", contextMenuAttrName) - aNode.removeAttribute(contextMenuAttrName); - } else if (currentContextMenu == contextMenuForPlace) { - aNode.removeAttribute(contextMenuAttrName); - } - - // Only add listeners for newly created wrappers: - if (!aIsUpdate) { - wrapper.addEventListener("mousedown", this); - wrapper.addEventListener("mouseup", this); - } - - return wrapper; - }, - - deferredUnwrapToolbarItem: function(aWrapper) { - return new Promise(resolve => { - dispatchFunction(() => { - let item = null; - try { - item = this.unwrapToolbarItem(aWrapper); - } catch (ex) { - Cu.reportError(ex); - } - resolve(item); - }); - }); - }, - - unwrapToolbarItem: function(aWrapper) { - if (aWrapper.nodeName != "toolbarpaletteitem") { - return aWrapper; - } - aWrapper.removeEventListener("mousedown", this); - aWrapper.removeEventListener("mouseup", this); - - let place = aWrapper.getAttribute("place"); - - let toolbarItem = aWrapper.firstChild; - if (!toolbarItem) { - log.error("no toolbarItem child for " + aWrapper.tagName + "#" + aWrapper.id); - aWrapper.remove(); - return null; - } - - if (aWrapper.hasAttribute("itemobserves")) { - toolbarItem.setAttribute("observes", aWrapper.getAttribute("itemobserves")); - } - - if (aWrapper.hasAttribute("itemchecked")) { - toolbarItem.checked = true; - } - - if (aWrapper.hasAttribute("itemcommand")) { - let commandID = aWrapper.getAttribute("itemcommand"); - toolbarItem.setAttribute("command", commandID); - - // XXX Bug 309953 - toolbarbuttons aren't in sync with their commands after customizing - let command = this.document.getElementById(commandID); - if (command && command.hasAttribute("disabled")) { - toolbarItem.setAttribute("disabled", command.getAttribute("disabled")); - } - } - - let wrappedContext = toolbarItem.getAttribute("wrapped-context"); - if (wrappedContext) { - let contextAttrName = toolbarItem.getAttribute("wrapped-contextAttrName"); - toolbarItem.setAttribute(contextAttrName, wrappedContext); - toolbarItem.removeAttribute("wrapped-contextAttrName"); - toolbarItem.removeAttribute("wrapped-context"); - } else if (place == "panel") { - toolbarItem.setAttribute("context", kPanelItemContextMenu); - } - - if (aWrapper.parentNode) { - aWrapper.parentNode.replaceChild(toolbarItem, aWrapper); - } - return toolbarItem; - }, - - _wrapToolbarItem: function*(aArea) { - let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.window); - if (!target || this.areas.has(target)) { - return null; - } - - this._addDragHandlers(target); - for (let child of target.children) { - if (this.isCustomizableItem(child) && !this.isWrappedToolbarItem(child)) { - yield this.deferredWrapToolbarItem(child, CustomizableUI.getPlaceForItem(child)).then(null, log.error); - } - } - this.areas.add(target); - return target; - }, - - _wrapToolbarItemSync: function(aArea) { - let target = CustomizableUI.getCustomizeTargetForArea(aArea, this.window); - if (!target || this.areas.has(target)) { - return null; - } - - this._addDragHandlers(target); - try { - for (let child of target.children) { - if (this.isCustomizableItem(child) && !this.isWrappedToolbarItem(child)) { - this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child)); - } - } - } catch (ex) { - log.error(ex, ex.stack); - } - - this.areas.add(target); - return target; - }, - - _wrapToolbarItems: function*() { - for (let area of CustomizableUI.areas) { - yield this._wrapToolbarItem(area); - } - }, - - _addDragHandlers: function(aTarget) { - aTarget.addEventListener("dragstart", this, true); - aTarget.addEventListener("dragover", this, true); - aTarget.addEventListener("dragexit", this, true); - aTarget.addEventListener("drop", this, true); - aTarget.addEventListener("dragend", this, true); - }, - - _wrapItemsInArea: function(target) { - for (let child of target.children) { - if (this.isCustomizableItem(child)) { - this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child)); - } - } - }, - - _removeDragHandlers: function(aTarget) { - aTarget.removeEventListener("dragstart", this, true); - aTarget.removeEventListener("dragover", this, true); - aTarget.removeEventListener("dragexit", this, true); - aTarget.removeEventListener("drop", this, true); - aTarget.removeEventListener("dragend", this, true); - }, - - _unwrapItemsInArea: function(target) { - for (let toolbarItem of target.children) { - if (this.isWrappedToolbarItem(toolbarItem)) { - this.unwrapToolbarItem(toolbarItem); - } - } - }, - - _unwrapToolbarItems: function() { - return Task.spawn(function*() { - for (let target of this.areas) { - for (let toolbarItem of target.children) { - if (this.isWrappedToolbarItem(toolbarItem)) { - yield this.deferredUnwrapToolbarItem(toolbarItem); - } - } - this._removeDragHandlers(target); - } - this.areas.clear(); - }.bind(this)).then(null, log.error); - }, - - _removeExtraToolbarsIfEmpty: function() { - let toolbox = this.window.gNavToolbox; - for (let child of toolbox.children) { - if (child.hasAttribute("customindex")) { - let placements = CustomizableUI.getWidgetIdsInArea(child.id); - if (!placements.length) { - CustomizableUI.removeExtraToolbar(child.id); - } - } - } - }, - - persistCurrentSets: function(aSetBeforePersisting) { - let document = this.document; - let toolbars = document.querySelectorAll("toolbar[customizable='true'][currentset]"); - for (let toolbar of toolbars) { - if (aSetBeforePersisting) { - let set = toolbar.currentSet; - toolbar.setAttribute("currentset", set); - } - // Persist the currentset attribute directly on hardcoded toolbars. - document.persist(toolbar.id, "currentset"); - } - }, - - reset: function() { - this.resetting = true; - // Disable the reset button temporarily while resetting: - let btn = this.document.getElementById("customization-reset-button"); - BrowserUITelemetry.countCustomizationEvent("reset"); - btn.disabled = true; - return Task.spawn(function*() { - this._removePanelCustomizationPlaceholders(); - yield this.depopulatePalette(); - yield this._unwrapToolbarItems(); - - CustomizableUI.reset(); - - this._updateLWThemeButtonIcon(); - - yield this._wrapToolbarItems(); - this.populatePalette(); - - this.persistCurrentSets(true); - - this._updateResetButton(); - this._updateUndoResetButton(); - this._updateEmptyPaletteNotice(); - this._showPanelCustomizationPlaceholders(); - this.resetting = false; - if (!this._wantToBeInCustomizeMode) { - this.exit(); - } - }.bind(this)).then(null, log.error); - }, - - undoReset: function() { - this.resetting = true; - - return Task.spawn(function*() { - this._removePanelCustomizationPlaceholders(); - yield this.depopulatePalette(); - yield this._unwrapToolbarItems(); - - CustomizableUI.undoReset(); - - this._updateLWThemeButtonIcon(); - - yield this._wrapToolbarItems(); - this.populatePalette(); - - this.persistCurrentSets(true); - - this._updateResetButton(); - this._updateUndoResetButton(); - this._updateEmptyPaletteNotice(); - this.resetting = false; - }.bind(this)).then(null, log.error); - }, - - _onToolbarVisibilityChange: function(aEvent) { - let toolbar = aEvent.target; - if (aEvent.detail.visible && toolbar.getAttribute("customizable") == "true") { - toolbar.setAttribute("customizing", "true"); - } else { - toolbar.removeAttribute("customizing"); - } - this._onUIChange(); - this.updateLWTStyling(); - }, - - onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) { - this._onUIChange(); - }, - - onWidgetAdded: function(aWidgetId, aArea, aPosition) { - this._onUIChange(); - }, - - onWidgetRemoved: function(aWidgetId, aArea) { - this._onUIChange(); - }, - - onWidgetBeforeDOMChange: function(aNodeToChange, aSecondaryNode, aContainer) { - if (aContainer.ownerGlobal != this.window || this.resetting) { - return; - } - if (aContainer.id == CustomizableUI.AREA_PANEL) { - this._removePanelCustomizationPlaceholders(); - } - // If we get called for widgets that aren't in the window yet, they might not have - // a parentNode at all. - if (aNodeToChange.parentNode) { - this.unwrapToolbarItem(aNodeToChange.parentNode); - } - if (aSecondaryNode) { - this.unwrapToolbarItem(aSecondaryNode.parentNode); - } - }, - - onWidgetAfterDOMChange: function(aNodeToChange, aSecondaryNode, aContainer) { - if (aContainer.ownerGlobal != this.window || this.resetting) { - return; - } - // If the node is still attached to the container, wrap it again: - if (aNodeToChange.parentNode) { - let place = CustomizableUI.getPlaceForItem(aNodeToChange); - this.wrapToolbarItem(aNodeToChange, place); - if (aSecondaryNode) { - this.wrapToolbarItem(aSecondaryNode, place); - } - } else { - // If not, it got removed. - - // If an API-based widget is removed while customizing, append it to the palette. - // The _applyDrop code itself will take care of positioning it correctly, if - // applicable. We need the code to be here so removing widgets using CustomizableUI's - // API also does the right thing (and adds it to the palette) - let widgetId = aNodeToChange.id; - let widget = CustomizableUI.getWidget(widgetId); - if (widget.provider == CustomizableUI.PROVIDER_API) { - let paletteItem = this.makePaletteItem(widget, "palette"); - this.visiblePalette.appendChild(paletteItem); - } - } - if (aContainer.id == CustomizableUI.AREA_PANEL) { - this._showPanelCustomizationPlaceholders(); - } - }, - - onWidgetDestroyed: function(aWidgetId) { - let wrapper = this.document.getElementById("wrapper-" + aWidgetId); - if (wrapper) { - let wasInPanel = wrapper.parentNode == this.panelUIContents; - wrapper.remove(); - if (wasInPanel) { - this._showPanelCustomizationPlaceholders(); - } - } - }, - - onWidgetAfterCreation: function(aWidgetId, aArea) { - // If the node was added to an area, we would have gotten an onWidgetAdded notification, - // plus associated DOM change notifications, so only do stuff for the palette: - if (!aArea) { - let widgetNode = this.document.getElementById(aWidgetId); - if (widgetNode) { - this.wrapToolbarItem(widgetNode, "palette"); - } else { - let widget = CustomizableUI.getWidget(aWidgetId); - this.visiblePalette.appendChild(this.makePaletteItem(widget, "palette")); - } - } - }, - - onAreaNodeRegistered: function(aArea, aContainer) { - if (aContainer.ownerDocument == this.document) { - this._wrapItemsInArea(aContainer); - this._addDragHandlers(aContainer); - DragPositionManager.add(this.window, aArea, aContainer); - this.areas.add(aContainer); - } - }, - - onAreaNodeUnregistered: function(aArea, aContainer, aReason) { - if (aContainer.ownerDocument == this.document && aReason == CustomizableUI.REASON_AREA_UNREGISTERED) { - this._unwrapItemsInArea(aContainer); - this._removeDragHandlers(aContainer); - DragPositionManager.remove(this.window, aArea, aContainer); - this.areas.delete(aContainer); - } - }, - - openAddonsManagerThemes: function(aEvent) { - aEvent.target.parentNode.parentNode.hidePopup(); - this.window.BrowserOpenAddonsMgr('addons://list/theme'); - }, - - getMoreThemes: function(aEvent) { - aEvent.target.parentNode.parentNode.hidePopup(); - let getMoreURL = Services.urlFormatter.formatURLPref("lightweightThemes.getMoreURL"); - this.window.openUILinkIn(getMoreURL, "tab"); - }, - - onLWThemesMenuShowing: function(aEvent) { - const DEFAULT_THEME_ID = "{972ce4c6-7e08-4474-a285-3208198ce6fd}"; - const RECENT_LWT_COUNT = 5; - - this._clearLWThemesMenu(aEvent.target); - - function previewTheme(aEvent) { - LightweightThemeManager.previewTheme(aEvent.target.theme.id != DEFAULT_THEME_ID ? - aEvent.target.theme : null); - } - - function resetPreview() { - LightweightThemeManager.resetPreview(); - } - - let onThemeSelected = panel => { - this._updateLWThemeButtonIcon(); - this._onUIChange(); - panel.hidePopup(); - }; - - AddonManager.getAddonByID(DEFAULT_THEME_ID, function(aDefaultTheme) { - let doc = this.window.document; - - function buildToolbarButton(aTheme) { - let tbb = doc.createElement("toolbarbutton"); - tbb.theme = aTheme; - tbb.setAttribute("label", aTheme.name); - if (aDefaultTheme == aTheme) { - // The actual icon is set up so it looks nice in about:addons, but - // we'd like the version that's correct for the OS we're on, so we set - // an attribute that our styling will then use to display the icon. - tbb.setAttribute("defaulttheme", "true"); - } else { - tbb.setAttribute("image", aTheme.iconURL); - } - if (aTheme.description) - tbb.setAttribute("tooltiptext", aTheme.description); - tbb.setAttribute("tabindex", "0"); - tbb.classList.add("customization-lwtheme-menu-theme"); - tbb.setAttribute("aria-checked", aTheme.isActive); - tbb.setAttribute("role", "menuitemradio"); - if (aTheme.isActive) { - tbb.setAttribute("active", "true"); - } - tbb.addEventListener("focus", previewTheme); - tbb.addEventListener("mouseover", previewTheme); - tbb.addEventListener("blur", resetPreview); - tbb.addEventListener("mouseout", resetPreview); - - return tbb; - } - - let themes = [aDefaultTheme]; - let lwts = LightweightThemeManager.usedThemes; - if (lwts.length > RECENT_LWT_COUNT) - lwts.length = RECENT_LWT_COUNT; - let currentLwt = LightweightThemeManager.currentTheme; - for (let lwt of lwts) { - lwt.isActive = !!currentLwt && (lwt.id == currentLwt.id); - themes.push(lwt); - } - - let footer = doc.getElementById("customization-lwtheme-menu-footer"); - let panel = footer.parentNode; - let recommendedLabel = doc.getElementById("customization-lwtheme-menu-recommended"); - for (let theme of themes) { - let button = buildToolbarButton(theme); - button.addEventListener("command", () => { - if ("userDisabled" in button.theme) - button.theme.userDisabled = false; - else - LightweightThemeManager.currentTheme = button.theme; - onThemeSelected(panel); - }); - panel.insertBefore(button, recommendedLabel); - } - - let lwthemePrefs = Services.prefs.getBranch("lightweightThemes."); - let recommendedThemes = lwthemePrefs.getComplexValue("recommendedThemes", - Ci.nsISupportsString).data; - recommendedThemes = JSON.parse(recommendedThemes); - let sb = Services.strings.createBundle("chrome://browser/locale/lightweightThemes.properties"); - for (let theme of recommendedThemes) { - theme.name = sb.GetStringFromName("lightweightThemes." + theme.id + ".name"); - theme.description = sb.GetStringFromName("lightweightThemes." + theme.id + ".description"); - let button = buildToolbarButton(theme); - button.addEventListener("command", () => { - LightweightThemeManager.setLocalTheme(button.theme); - recommendedThemes = recommendedThemes.filter((aTheme) => { return aTheme.id != button.theme.id; }); - let string = Cc["@mozilla.org/supports-string;1"] - .createInstance(Ci.nsISupportsString); - string.data = JSON.stringify(recommendedThemes); - lwthemePrefs.setComplexValue("recommendedThemes", - Ci.nsISupportsString, string); - onThemeSelected(panel); - }); - panel.insertBefore(button, footer); - } - let hideRecommendedLabel = (footer.previousSibling == recommendedLabel); - recommendedLabel.hidden = hideRecommendedLabel; - }.bind(this)); - }, - - _clearLWThemesMenu: function(panel) { - let footer = this.document.getElementById("customization-lwtheme-menu-footer"); - let recommendedLabel = this.document.getElementById("customization-lwtheme-menu-recommended"); - for (let element of [footer, recommendedLabel]) { - while (element.previousSibling && - element.previousSibling.localName == "toolbarbutton") { - element.previousSibling.remove(); - } - } - - // Workaround for bug 1059934 - panel.removeAttribute("height"); - }, - - _onUIChange: function() { - this._changed = true; - if (!this.resetting) { - this._updateResetButton(); - this._updateUndoResetButton(); - this._updateEmptyPaletteNotice(); - } - CustomizableUI.dispatchToolboxEvent("customizationchange"); - }, - - _updateEmptyPaletteNotice: function() { - let paletteItems = this.visiblePalette.getElementsByTagName("toolbarpaletteitem"); - this.paletteEmptyNotice.hidden = !!paletteItems.length; - }, - - _updateResetButton: function() { - let btn = this.document.getElementById("customization-reset-button"); - btn.disabled = CustomizableUI.inDefaultState; - }, - - _updateUndoResetButton: function() { - let undoResetButton = this.document.getElementById("customization-undo-reset-button"); - undoResetButton.hidden = !CustomizableUI.canUndoReset; - }, - - handleEvent: function(aEvent) { - switch (aEvent.type) { - case "toolbarvisibilitychange": - this._onToolbarVisibilityChange(aEvent); - break; - case "dragstart": - this._onDragStart(aEvent); - break; - case "dragover": - this._onDragOver(aEvent); - break; - case "drop": - this._onDragDrop(aEvent); - break; - case "dragexit": - this._onDragExit(aEvent); - break; - case "dragend": - this._onDragEnd(aEvent); - break; - case "command": - if (aEvent.originalTarget == this.window.PanelUI.menuButton) { - this.exit(); - aEvent.preventDefault(); - } - break; - case "mousedown": - this._onMouseDown(aEvent); - break; - case "mouseup": - this._onMouseUp(aEvent); - break; - case "keypress": - if (aEvent.keyCode == aEvent.DOM_VK_ESCAPE) { - this.exit(); - } - break; - case "unload": - this.uninit(); - break; - } - }, - - observe: function(aSubject, aTopic, aData) { - switch (aTopic) { - case "nsPref:changed": - this._updateResetButton(); - this._updateUndoResetButton(); - if (AppConstants.CAN_DRAW_IN_TITLEBAR) { - this._updateTitlebarButton(); - } - break; - case "lightweight-theme-window-updated": - if (aSubject == this.window) { - aData = JSON.parse(aData); - if (!aData) { - this.removeLWTStyling(); - } else { - this.updateLWTStyling(aData); - } - } - break; - } - }, - - _updateTitlebarButton: function() { - if (!AppConstants.CAN_DRAW_IN_TITLEBAR) { - return; - } - let drawInTitlebar = true; - try { - drawInTitlebar = Services.prefs.getBoolPref(kDrawInTitlebarPref); - } catch (ex) { } - let button = this.document.getElementById("customization-titlebar-visibility-button"); - // Drawing in the titlebar means 'hiding' the titlebar: - if (drawInTitlebar) { - button.removeAttribute("checked"); - } else { - button.setAttribute("checked", "true"); - } - }, - - toggleTitlebar: function(aShouldShowTitlebar) { - if (!AppConstants.CAN_DRAW_IN_TITLEBAR) { - return; - } - // Drawing in the titlebar means not showing the titlebar, hence the negation: - Services.prefs.setBoolPref(kDrawInTitlebarPref, !aShouldShowTitlebar); - }, - - _onDragStart: function(aEvent) { - __dumpDragData(aEvent); - let item = aEvent.target; - while (item && item.localName != "toolbarpaletteitem") { - if (item.localName == "toolbar") { - return; - } - item = item.parentNode; - } - - let draggedItem = item.firstChild; - let placeForItem = CustomizableUI.getPlaceForItem(item); - let isRemovable = placeForItem == "palette" || - CustomizableUI.isWidgetRemovable(draggedItem); - if (item.classList.contains(kPlaceholderClass) || !isRemovable) { - return; - } - - let dt = aEvent.dataTransfer; - let documentId = aEvent.target.ownerDocument.documentElement.id; - let isInToolbar = placeForItem == "toolbar"; - - dt.mozSetDataAt(kDragDataTypePrefix + documentId, draggedItem.id, 0); - dt.effectAllowed = "move"; - - let itemRect = draggedItem.getBoundingClientRect(); - let itemCenter = {x: itemRect.left + itemRect.width / 2, - y: itemRect.top + itemRect.height / 2}; - this._dragOffset = {x: aEvent.clientX - itemCenter.x, - y: aEvent.clientY - itemCenter.y}; - - gDraggingInToolbars = new Set(); - - // Hack needed so that the dragimage will still show the - // item as it appeared before it was hidden. - this._initializeDragAfterMove = function() { - // For automated tests, we sometimes start exiting customization mode - // before this fires, which leaves us with placeholders inserted after - // we've exited. So we need to check that we are indeed customizing. - if (this._customizing && !this._transitioning) { - item.hidden = true; - this._showPanelCustomizationPlaceholders(); - DragPositionManager.start(this.window); - if (item.nextSibling) { - this._setDragActive(item.nextSibling, "before", draggedItem.id, isInToolbar); - this._dragOverItem = item.nextSibling; - } else if (isInToolbar && item.previousSibling) { - this._setDragActive(item.previousSibling, "after", draggedItem.id, isInToolbar); - this._dragOverItem = item.previousSibling; - } - } - this._initializeDragAfterMove = null; - this.window.clearTimeout(this._dragInitializeTimeout); - }.bind(this); - this._dragInitializeTimeout = this.window.setTimeout(this._initializeDragAfterMove, 0); - }, - - _onDragOver: function(aEvent) { - if (this._isUnwantedDragDrop(aEvent)) { - return; - } - if (this._initializeDragAfterMove) { - this._initializeDragAfterMove(); - } - - __dumpDragData(aEvent); - - let document = aEvent.target.ownerDocument; - let documentId = document.documentElement.id; - if (!aEvent.dataTransfer.mozTypesAt(0)) { - return; - } - - let draggedItemId = - aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0); - let draggedWrapper = document.getElementById("wrapper-" + draggedItemId); - let targetArea = this._getCustomizableParent(aEvent.currentTarget); - let originArea = this._getCustomizableParent(draggedWrapper); - - // Do nothing if the target or origin are not customizable. - if (!targetArea || !originArea) { - return; - } - - // Do nothing if the widget is not allowed to be removed. - if (targetArea.id == kPaletteId && - !CustomizableUI.isWidgetRemovable(draggedItemId)) { - return; - } - - // Do nothing if the widget is not allowed to move to the target area. - if (targetArea.id != kPaletteId && - !CustomizableUI.canWidgetMoveToArea(draggedItemId, targetArea.id)) { - return; - } - - let targetIsToolbar = CustomizableUI.getAreaType(targetArea.id) == "toolbar"; - let targetNode = this._getDragOverNode(aEvent, targetArea, targetIsToolbar, draggedItemId); - - // We need to determine the place that the widget is being dropped in - // the target. - let dragOverItem, dragValue; - if (targetNode == targetArea.customizationTarget) { - // We'll assume if the user is dragging directly over the target, that - // they're attempting to append a child to that target. - dragOverItem = (targetIsToolbar ? this._findVisiblePreviousSiblingNode(targetNode.lastChild) : - targetNode.lastChild) || targetNode; - dragValue = "after"; - } else { - let targetParent = targetNode.parentNode; - let position = Array.indexOf(targetParent.children, targetNode); - if (position == -1) { - dragOverItem = targetIsToolbar ? this._findVisiblePreviousSiblingNode(targetNode.lastChild) : - targetParent.lastChild; - dragValue = "after"; - } else { - dragOverItem = targetParent.children[position]; - if (!targetIsToolbar) { - dragValue = "before"; - } else { - // Check if the aDraggedItem is hovered past the first half of dragOverItem - let window = dragOverItem.ownerGlobal; - let direction = window.getComputedStyle(dragOverItem, null).direction; - let itemRect = dragOverItem.getBoundingClientRect(); - let dropTargetCenter = itemRect.left + (itemRect.width / 2); - let existingDir = dragOverItem.getAttribute("dragover"); - if ((existingDir == "before") == (direction == "ltr")) { - dropTargetCenter += (parseInt(dragOverItem.style.borderLeftWidth) || 0) / 2; - } else { - dropTargetCenter -= (parseInt(dragOverItem.style.borderRightWidth) || 0) / 2; - } - let before = direction == "ltr" ? aEvent.clientX < dropTargetCenter : aEvent.clientX > dropTargetCenter; - dragValue = before ? "before" : "after"; - } - } - } - - if (this._dragOverItem && dragOverItem != this._dragOverItem) { - this._cancelDragActive(this._dragOverItem, dragOverItem); - } - - if (dragOverItem != this._dragOverItem || dragValue != dragOverItem.getAttribute("dragover")) { - if (dragOverItem != targetArea.customizationTarget) { - this._setDragActive(dragOverItem, dragValue, draggedItemId, targetIsToolbar); - } else if (targetIsToolbar) { - this._updateToolbarCustomizationOutline(this.window, targetArea); - } - this._dragOverItem = dragOverItem; - } - - aEvent.preventDefault(); - aEvent.stopPropagation(); - }, - - _onDragDrop: function(aEvent) { - if (this._isUnwantedDragDrop(aEvent)) { - return; - } - - __dumpDragData(aEvent); - this._initializeDragAfterMove = null; - this.window.clearTimeout(this._dragInitializeTimeout); - - let targetArea = this._getCustomizableParent(aEvent.currentTarget); - let document = aEvent.target.ownerDocument; - let documentId = document.documentElement.id; - let draggedItemId = - aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0); - let draggedWrapper = document.getElementById("wrapper-" + draggedItemId); - let originArea = this._getCustomizableParent(draggedWrapper); - if (this._dragSizeMap) { - this._dragSizeMap = new WeakMap(); - } - // Do nothing if the target area or origin area are not customizable. - if (!targetArea || !originArea) { - return; - } - let targetNode = this._dragOverItem; - let dropDir = targetNode.getAttribute("dragover"); - // Need to insert *after* this node if we promised the user that: - if (targetNode != targetArea && dropDir == "after") { - if (targetNode.nextSibling) { - targetNode = targetNode.nextSibling; - } else { - targetNode = targetArea; - } - } - // If the target node is a placeholder, get its sibling as the real target. - while (targetNode.classList.contains(kPlaceholderClass) && targetNode.nextSibling) { - targetNode = targetNode.nextSibling; - } - if (targetNode.tagName == "toolbarpaletteitem") { - targetNode = targetNode.firstChild; - } - - this._cancelDragActive(this._dragOverItem, null, true); - this._removePanelCustomizationPlaceholders(); - - try { - this._applyDrop(aEvent, targetArea, originArea, draggedItemId, targetNode); - } catch (ex) { - log.error(ex, ex.stack); - } - - this._showPanelCustomizationPlaceholders(); - }, - - _applyDrop: function(aEvent, aTargetArea, aOriginArea, aDraggedItemId, aTargetNode) { - let document = aEvent.target.ownerDocument; - let draggedItem = document.getElementById(aDraggedItemId); - draggedItem.hidden = false; - draggedItem.removeAttribute("mousedown"); - - // Do nothing if the target was dropped onto itself (ie, no change in area - // or position). - if (draggedItem == aTargetNode) { - return; - } - - // Is the target area the customization palette? - if (aTargetArea.id == kPaletteId) { - // Did we drag from outside the palette? - if (aOriginArea.id !== kPaletteId) { - if (!CustomizableUI.isWidgetRemovable(aDraggedItemId)) { - return; - } - - CustomizableUI.removeWidgetFromArea(aDraggedItemId); - BrowserUITelemetry.countCustomizationEvent("remove"); - // Special widgets are removed outright, we can return here: - if (CustomizableUI.isSpecialWidget(aDraggedItemId)) { - return; - } - } - draggedItem = draggedItem.parentNode; - - // If the target node is the palette itself, just append - if (aTargetNode == this.visiblePalette) { - this.visiblePalette.appendChild(draggedItem); - } else { - // The items in the palette are wrapped, so we need the target node's parent here: - this.visiblePalette.insertBefore(draggedItem, aTargetNode.parentNode); - } - if (aOriginArea.id !== kPaletteId) { - // The dragend event already fires when the item moves within the palette. - this._onDragEnd(aEvent); - } - return; - } - - if (!CustomizableUI.canWidgetMoveToArea(aDraggedItemId, aTargetArea.id)) { - return; - } - - // Skipintoolbarset items won't really be moved: - if (draggedItem.getAttribute("skipintoolbarset") == "true") { - // These items should never leave their area: - if (aTargetArea != aOriginArea) { - return; - } - let place = draggedItem.parentNode.getAttribute("place"); - this.unwrapToolbarItem(draggedItem.parentNode); - if (aTargetNode == aTargetArea.customizationTarget) { - aTargetArea.customizationTarget.appendChild(draggedItem); - } else { - this.unwrapToolbarItem(aTargetNode.parentNode); - aTargetArea.customizationTarget.insertBefore(draggedItem, aTargetNode); - this.wrapToolbarItem(aTargetNode, place); - } - this.wrapToolbarItem(draggedItem, place); - BrowserUITelemetry.countCustomizationEvent("move"); - return; - } - - // Is the target the customization area itself? If so, we just add the - // widget to the end of the area. - if (aTargetNode == aTargetArea.customizationTarget) { - CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id); - // For the purposes of BrowserUITelemetry, we consider both moving a widget - // within the same area, and adding a widget from one area to another area - // as a "move". An "add" is only when we move an item from the palette into - // an area. - let custEventType = aOriginArea.id == kPaletteId ? "add" : "move"; - BrowserUITelemetry.countCustomizationEvent(custEventType); - this._onDragEnd(aEvent); - return; - } - - // We need to determine the place that the widget is being dropped in - // the target. - let placement; - let itemForPlacement = aTargetNode; - // Skip the skipintoolbarset items when determining the place of the item: - while (itemForPlacement && itemForPlacement.getAttribute("skipintoolbarset") == "true" && - itemForPlacement.parentNode && - itemForPlacement.parentNode.nodeName == "toolbarpaletteitem") { - itemForPlacement = itemForPlacement.parentNode.nextSibling; - if (itemForPlacement && itemForPlacement.nodeName == "toolbarpaletteitem") { - itemForPlacement = itemForPlacement.firstChild; - } - } - if (itemForPlacement && !itemForPlacement.classList.contains(kPlaceholderClass)) { - let targetNodeId = (itemForPlacement.nodeName == "toolbarpaletteitem") ? - itemForPlacement.firstChild && itemForPlacement.firstChild.id : - itemForPlacement.id; - placement = CustomizableUI.getPlacementOfWidget(targetNodeId); - } - if (!placement) { - log.debug("Could not get a position for " + aTargetNode.nodeName + "#" + aTargetNode.id + "." + aTargetNode.className); - } - let position = placement ? placement.position : null; - - // Is the target area the same as the origin? Since we've already handled - // the possibility that the target is the customization palette, we know - // that the widget is moving within a customizable area. - if (aTargetArea == aOriginArea) { - CustomizableUI.moveWidgetWithinArea(aDraggedItemId, position); - } else { - CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id, position); - } - - this._onDragEnd(aEvent); - - // For BrowserUITelemetry, an "add" is only when we move an item from the palette - // into an area. Otherwise, it's a move. - let custEventType = aOriginArea.id == kPaletteId ? "add" : "move"; - BrowserUITelemetry.countCustomizationEvent(custEventType); - - // If we dropped onto a skipintoolbarset item, manually correct the drop location: - if (aTargetNode != itemForPlacement) { - let draggedWrapper = draggedItem.parentNode; - let container = draggedWrapper.parentNode; - container.insertBefore(draggedWrapper, aTargetNode.parentNode); - } - }, - - _onDragExit: function(aEvent) { - if (this._isUnwantedDragDrop(aEvent)) { - return; - } - - __dumpDragData(aEvent); - - // When leaving customization areas, cancel the drag on the last dragover item - // We've attached the listener to areas, so aEvent.currentTarget will be the area. - // We don't care about dragexit events fired on descendants of the area, - // so we check that the event's target is the same as the area to which the listener - // was attached. - if (this._dragOverItem && aEvent.target == aEvent.currentTarget) { - this._cancelDragActive(this._dragOverItem); - this._dragOverItem = null; - } - }, - - /** - * To workaround bug 460801 we manually forward the drop event here when dragend wouldn't be fired. - */ - _onDragEnd: function(aEvent) { - if (this._isUnwantedDragDrop(aEvent)) { - return; - } - this._initializeDragAfterMove = null; - this.window.clearTimeout(this._dragInitializeTimeout); - __dumpDragData(aEvent, "_onDragEnd"); - - let document = aEvent.target.ownerDocument; - document.documentElement.removeAttribute("customizing-movingItem"); - - let documentId = document.documentElement.id; - if (!aEvent.dataTransfer.mozTypesAt(0)) { - return; - } - - let draggedItemId = - aEvent.dataTransfer.mozGetDataAt(kDragDataTypePrefix + documentId, 0); - - let draggedWrapper = document.getElementById("wrapper-" + draggedItemId); - - // DraggedWrapper might no longer available if a widget node is - // destroyed after starting (but before stopping) a drag. - if (draggedWrapper) { - draggedWrapper.hidden = false; - draggedWrapper.removeAttribute("mousedown"); - } - - if (this._dragOverItem) { - this._cancelDragActive(this._dragOverItem); - this._dragOverItem = null; - } - this._updateToolbarCustomizationOutline(this.window); - this._showPanelCustomizationPlaceholders(); - DragPositionManager.stop(); - }, - - _isUnwantedDragDrop: function(aEvent) { - // The simulated events generated by synthesizeDragStart/synthesizeDrop in - // mochitests are used only for testing whether the right data is being put - // into the dataTransfer. Neither cause a real drop to occur, so they don't - // set the source node. There isn't a means of testing real drag and drops, - // so this pref skips the check but it should only be set by test code. - if (this._skipSourceNodeCheck) { - return false; - } - - /* Discard drag events that originated from a separate window to - prevent content->chrome privilege escalations. */ - let mozSourceNode = aEvent.dataTransfer.mozSourceNode; - // mozSourceNode is null in the dragStart event handler or if - // the drag event originated in an external application. - return !mozSourceNode || - mozSourceNode.ownerGlobal != this.window; - }, - - _setDragActive: function(aItem, aValue, aDraggedItemId, aInToolbar) { - if (!aItem) { - return; - } - - if (aItem.getAttribute("dragover") != aValue) { - aItem.setAttribute("dragover", aValue); - - let window = aItem.ownerGlobal; - let draggedItem = window.document.getElementById(aDraggedItemId); - if (!aInToolbar) { - this._setGridDragActive(aItem, draggedItem, aValue); - } else { - let targetArea = this._getCustomizableParent(aItem); - this._updateToolbarCustomizationOutline(window, targetArea); - let makeSpaceImmediately = false; - if (!gDraggingInToolbars.has(targetArea.id)) { - gDraggingInToolbars.add(targetArea.id); - let draggedWrapper = this.document.getElementById("wrapper-" + aDraggedItemId); - let originArea = this._getCustomizableParent(draggedWrapper); - makeSpaceImmediately = originArea == targetArea; - } - // Calculate width of the item when it'd be dropped in this position - let width = this._getDragItemSize(aItem, draggedItem).width; - let direction = window.getComputedStyle(aItem).direction; - let prop, otherProp; - // If we're inserting before in ltr, or after in rtl: - if ((aValue == "before") == (direction == "ltr")) { - prop = "borderLeftWidth"; - otherProp = "border-right-width"; - } else { - // otherwise: - prop = "borderRightWidth"; - otherProp = "border-left-width"; - } - if (makeSpaceImmediately) { - aItem.setAttribute("notransition", "true"); - } - aItem.style[prop] = width + 'px'; - aItem.style.removeProperty(otherProp); - if (makeSpaceImmediately) { - // Force a layout flush: - aItem.getBoundingClientRect(); - aItem.removeAttribute("notransition"); - } - } - } - }, - _cancelDragActive: function(aItem, aNextItem, aNoTransition) { - this._updateToolbarCustomizationOutline(aItem.ownerGlobal); - let currentArea = this._getCustomizableParent(aItem); - if (!currentArea) { - return; - } - let isToolbar = CustomizableUI.getAreaType(currentArea.id) == "toolbar"; - if (isToolbar) { - if (aNoTransition) { - aItem.setAttribute("notransition", "true"); - } - aItem.removeAttribute("dragover"); - // Remove both property values in the case that the end padding - // had been set. - aItem.style.removeProperty("border-left-width"); - aItem.style.removeProperty("border-right-width"); - if (aNoTransition) { - // Force a layout flush: - aItem.getBoundingClientRect(); - aItem.removeAttribute("notransition"); - } - } else { - aItem.removeAttribute("dragover"); - if (aNextItem) { - let nextArea = this._getCustomizableParent(aNextItem); - if (nextArea == currentArea) { - // No need to do anything if we're still dragging in this area: - return; - } - } - // Otherwise, clear everything out: - let positionManager = DragPositionManager.getManagerForArea(currentArea); - positionManager.clearPlaceholders(currentArea, aNoTransition); - } - }, - - _setGridDragActive: function(aDragOverNode, aDraggedItem, aValue) { - let targetArea = this._getCustomizableParent(aDragOverNode); - let draggedWrapper = this.document.getElementById("wrapper-" + aDraggedItem.id); - let originArea = this._getCustomizableParent(draggedWrapper); - let positionManager = DragPositionManager.getManagerForArea(targetArea); - let draggedSize = this._getDragItemSize(aDragOverNode, aDraggedItem); - let isWide = aDraggedItem.classList.contains(CustomizableUI.WIDE_PANEL_CLASS); - positionManager.insertPlaceholder(targetArea, aDragOverNode, isWide, draggedSize, - originArea == targetArea); - }, - - _getDragItemSize: function(aDragOverNode, aDraggedItem) { - // Cache it good, cache it real good. - if (!this._dragSizeMap) - this._dragSizeMap = new WeakMap(); - if (!this._dragSizeMap.has(aDraggedItem)) - this._dragSizeMap.set(aDraggedItem, new WeakMap()); - let itemMap = this._dragSizeMap.get(aDraggedItem); - let targetArea = this._getCustomizableParent(aDragOverNode); - let currentArea = this._getCustomizableParent(aDraggedItem); - // Return the size for this target from cache, if it exists. - let size = itemMap.get(targetArea); - if (size) - return size; - - // Calculate size of the item when it'd be dropped in this position. - let currentParent = aDraggedItem.parentNode; - let currentSibling = aDraggedItem.nextSibling; - const kAreaType = "cui-areatype"; - let areaType, currentType; - - if (targetArea != currentArea) { - // Move the widget temporarily next to the placeholder. - aDragOverNode.parentNode.insertBefore(aDraggedItem, aDragOverNode); - // Update the node's areaType. - areaType = CustomizableUI.getAreaType(targetArea.id); - currentType = aDraggedItem.hasAttribute(kAreaType) && - aDraggedItem.getAttribute(kAreaType); - if (areaType) - aDraggedItem.setAttribute(kAreaType, areaType); - this.wrapToolbarItem(aDraggedItem, areaType || "palette"); - CustomizableUI.onWidgetDrag(aDraggedItem.id, targetArea.id); - } else { - aDraggedItem.parentNode.hidden = false; - } - - // Fetch the new size. - let rect = aDraggedItem.parentNode.getBoundingClientRect(); - size = {width: rect.width, height: rect.height}; - // Cache the found value of size for this target. - itemMap.set(targetArea, size); - - if (targetArea != currentArea) { - this.unwrapToolbarItem(aDraggedItem.parentNode); - // Put the item back into its previous position. - currentParent.insertBefore(aDraggedItem, currentSibling); - // restore the areaType - if (areaType) { - if (currentType === false) - aDraggedItem.removeAttribute(kAreaType); - else - aDraggedItem.setAttribute(kAreaType, currentType); - } - this.createOrUpdateWrapper(aDraggedItem, null, true); - CustomizableUI.onWidgetDrag(aDraggedItem.id); - } else { - aDraggedItem.parentNode.hidden = true; - } - return size; - }, - - _getCustomizableParent: function(aElement) { - let areas = CustomizableUI.areas; - areas.push(kPaletteId); - while (aElement) { - if (areas.indexOf(aElement.id) != -1) { - return aElement; - } - aElement = aElement.parentNode; - } - return null; - }, - - _getDragOverNode: function(aEvent, aAreaElement, aInToolbar, aDraggedItemId) { - let expectedParent = aAreaElement.customizationTarget || aAreaElement; - // Our tests are stupid. Cope: - if (!aEvent.clientX && !aEvent.clientY) { - return aEvent.target; - } - // Offset the drag event's position with the offset to the center of - // the thing we're dragging - let dragX = aEvent.clientX - this._dragOffset.x; - let dragY = aEvent.clientY - this._dragOffset.y; - - // Ensure this is within the container - let boundsContainer = expectedParent; - // NB: because the panel UI itself is inside a scrolling container, we need - // to use the parent bounds (otherwise, if the panel UI is scrolled down, - // the numbers we get are in window coordinates which leads to various kinds - // of weirdness) - if (boundsContainer == this.panelUIContents) { - boundsContainer = boundsContainer.parentNode; - } - let bounds = boundsContainer.getBoundingClientRect(); - dragX = Math.min(bounds.right, Math.max(dragX, bounds.left)); - dragY = Math.min(bounds.bottom, Math.max(dragY, bounds.top)); - - let targetNode; - if (aInToolbar) { - targetNode = aAreaElement.ownerDocument.elementFromPoint(dragX, dragY); - while (targetNode && targetNode.parentNode != expectedParent) { - targetNode = targetNode.parentNode; - } - } else { - let positionManager = DragPositionManager.getManagerForArea(aAreaElement); - // Make it relative to the container: - dragX -= bounds.left; - // NB: but if we're in the panel UI, we need to use the actual panel - // contents instead of the scrolling container to determine our origin - // offset against: - if (expectedParent == this.panelUIContents) { - dragY -= this.panelUIContents.getBoundingClientRect().top; - } else { - dragY -= bounds.top; - } - // Find the closest node: - targetNode = positionManager.find(aAreaElement, dragX, dragY, aDraggedItemId); - } - return targetNode || aEvent.target; - }, - - _onMouseDown: function(aEvent) { - log.debug("_onMouseDown"); - if (aEvent.button != 0) { - return; - } - let doc = aEvent.target.ownerDocument; - doc.documentElement.setAttribute("customizing-movingItem", true); - let item = this._getWrapper(aEvent.target); - if (item && !item.classList.contains(kPlaceholderClass) && - item.getAttribute("removable") == "true") { - item.setAttribute("mousedown", "true"); - } - }, - - _onMouseUp: function(aEvent) { - log.debug("_onMouseUp"); - if (aEvent.button != 0) { - return; - } - let doc = aEvent.target.ownerDocument; - doc.documentElement.removeAttribute("customizing-movingItem"); - let item = this._getWrapper(aEvent.target); - if (item) { - item.removeAttribute("mousedown"); - } - }, - - _getWrapper: function(aElement) { - while (aElement && aElement.localName != "toolbarpaletteitem") { - if (aElement.localName == "toolbar") - return null; - aElement = aElement.parentNode; - } - return aElement; - }, - - _showPanelCustomizationPlaceholders: function() { - let doc = this.document; - let contents = this.panelUIContents; - let narrowItemsAfterWideItem = 0; - let node = contents.lastChild; - while (node && !node.classList.contains(CustomizableUI.WIDE_PANEL_CLASS) && - (!node.firstChild || !node.firstChild.classList.contains(CustomizableUI.WIDE_PANEL_CLASS))) { - if (!node.hidden && !node.classList.contains(kPlaceholderClass)) { - narrowItemsAfterWideItem++; - } - node = node.previousSibling; - } - - let orphanedItems = narrowItemsAfterWideItem % CustomizableUI.PANEL_COLUMN_COUNT; - let placeholders = CustomizableUI.PANEL_COLUMN_COUNT - orphanedItems; - - let currentPlaceholderCount = contents.querySelectorAll("." + kPlaceholderClass).length; - if (placeholders > currentPlaceholderCount) { - while (placeholders-- > currentPlaceholderCount) { - let placeholder = doc.createElement("toolbarpaletteitem"); - placeholder.classList.add(kPlaceholderClass); - // XXXjaws The toolbarbutton child here is only necessary to get - // the styling right here. - let placeholderChild = doc.createElement("toolbarbutton"); - placeholderChild.classList.add(kPlaceholderClass + "-child"); - placeholder.appendChild(placeholderChild); - contents.appendChild(placeholder); - } - } else if (placeholders < currentPlaceholderCount) { - while (placeholders++ < currentPlaceholderCount) { - contents.querySelectorAll("." + kPlaceholderClass)[0].remove(); - } - } - }, - - _removePanelCustomizationPlaceholders: function() { - let contents = this.panelUIContents; - let oldPlaceholders = contents.getElementsByClassName(kPlaceholderClass); - while (oldPlaceholders.length) { - contents.removeChild(oldPlaceholders[0]); - } - }, - - /** - * Update toolbar customization targets during drag events to add or remove - * outlines to indicate that an area is customizable. - * - * @param aWindow The XUL window in which outlines should be updated. - * @param {Element} [aToolbarArea=null] The element of the customizable toolbar area to add the - * outline to. If aToolbarArea is falsy, the outline will be - * removed from all toolbar areas. - */ - _updateToolbarCustomizationOutline: function(aWindow, aToolbarArea = null) { - // Remove the attribute from existing customization targets - for (let area of CustomizableUI.areas) { - if (CustomizableUI.getAreaType(area) != CustomizableUI.TYPE_TOOLBAR) { - continue; - } - let target = CustomizableUI.getCustomizeTargetForArea(area, aWindow); - target.removeAttribute("customizing-dragovertarget"); - } - - // Now set the attribute on the desired target - if (aToolbarArea) { - if (CustomizableUI.getAreaType(aToolbarArea.id) != CustomizableUI.TYPE_TOOLBAR) - return; - let target = CustomizableUI.getCustomizeTargetForArea(aToolbarArea.id, aWindow); - target.setAttribute("customizing-dragovertarget", true); - } - }, - - _findVisiblePreviousSiblingNode: function(aReferenceNode) { - while (aReferenceNode && - aReferenceNode.localName == "toolbarpaletteitem" && - aReferenceNode.firstChild.hidden) { - aReferenceNode = aReferenceNode.previousSibling; - } - return aReferenceNode; - }, -}; - -function __dumpDragData(aEvent, caller) { - if (!gDebug) { - return; - } - let str = "Dumping drag data (" + (caller ? caller + " in " : "") + "CustomizeMode.jsm) {\n"; - str += " type: " + aEvent["type"] + "\n"; - for (let el of ["target", "currentTarget", "relatedTarget"]) { - if (aEvent[el]) { - str += " " + el + ": " + aEvent[el] + "(localName=" + aEvent[el].localName + "; id=" + aEvent[el].id + ")\n"; - } - } - for (let prop in aEvent.dataTransfer) { - if (typeof aEvent.dataTransfer[prop] != "function") { - str += " dataTransfer[" + prop + "]: " + aEvent.dataTransfer[prop] + "\n"; - } - } - str += "}"; - log.debug(str); -} - -function dispatchFunction(aFunc) { - Services.tm.currentThread.dispatch(aFunc, Ci.nsIThread.DISPATCH_NORMAL); -} diff --git a/browser/components/customizableui/DragPositionManager.jsm b/browser/components/customizableui/DragPositionManager.jsm deleted file mode 100644 index 1b4eb59dc..000000000 --- a/browser/components/customizableui/DragPositionManager.jsm +++ /dev/null @@ -1,420 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -Components.utils.import("resource:///modules/CustomizableUI.jsm"); - -var gManagers = new WeakMap(); - -const kPaletteId = "customization-palette"; -const kPlaceholderClass = "panel-customization-placeholder"; - -this.EXPORTED_SYMBOLS = ["DragPositionManager"]; - -function AreaPositionManager(aContainer) { - // Caching the direction and bounds of the container for quick access later: - let window = aContainer.ownerGlobal; - this._dir = window.getComputedStyle(aContainer).direction; - let containerRect = aContainer.getBoundingClientRect(); - this._containerInfo = { - left: containerRect.left, - right: containerRect.right, - top: containerRect.top, - width: containerRect.width - }; - this._inPanel = aContainer.id == CustomizableUI.AREA_PANEL; - this._horizontalDistance = null; - this.update(aContainer); -} - -AreaPositionManager.prototype = { - _nodePositionStore: null, - _wideCache: null, - - update: function(aContainer) { - this._nodePositionStore = new WeakMap(); - this._wideCache = new Set(); - let last = null; - let singleItemHeight; - for (let child of aContainer.children) { - if (child.hidden) { - continue; - } - let isNodeWide = this._checkIfWide(child); - if (isNodeWide) { - this._wideCache.add(child.id); - } - let coordinates = this._lazyStoreGet(child); - // We keep a baseline horizontal distance between non-wide nodes around - // for use when we can't compare with previous/next nodes - if (!this._horizontalDistance && last && !isNodeWide) { - this._horizontalDistance = coordinates.left - last.left; - } - // We also keep the basic height of non-wide items for use below: - if (!isNodeWide && !singleItemHeight) { - singleItemHeight = coordinates.height; - } - last = !isNodeWide ? coordinates : null; - } - if (this._inPanel) { - this._heightToWidthFactor = CustomizableUI.PANEL_COLUMN_COUNT; - } else { - this._heightToWidthFactor = this._containerInfo.width / singleItemHeight; - } - }, - - /** - * Find the closest node in the container given the coordinates. - * "Closest" is defined in a somewhat strange manner: we prefer nodes - * which are in the same row over nodes that are in a different row. - * In order to implement this, we use a weighted cartesian distance - * where dy is more heavily weighted by a factor corresponding to the - * ratio between the container's width and the height of its elements. - */ - find: function(aContainer, aX, aY, aDraggedItemId) { - let closest = null; - let minCartesian = Number.MAX_VALUE; - let containerX = this._containerInfo.left; - let containerY = this._containerInfo.top; - for (let node of aContainer.children) { - let coordinates = this._lazyStoreGet(node); - let offsetX = coordinates.x - containerX; - let offsetY = coordinates.y - containerY; - let hDiff = offsetX - aX; - let vDiff = offsetY - aY; - // For wide widgets, we're always going to be further from the center - // horizontally. Compensate: - if (this.isWide(node)) { - hDiff /= CustomizableUI.PANEL_COLUMN_COUNT; - } - // Then compensate for the height/width ratio so that we prefer items - // which are in the same row: - hDiff /= this._heightToWidthFactor; - - let cartesianDiff = hDiff * hDiff + vDiff * vDiff; - if (cartesianDiff < minCartesian) { - minCartesian = cartesianDiff; - closest = node; - } - } - - // Now correct this node based on what we're dragging - if (closest) { - let doc = aContainer.ownerDocument; - let draggedItem = doc.getElementById(aDraggedItemId); - // If dragging a wide item, always pick the first item in a row: - if (this._inPanel && draggedItem && - draggedItem.classList.contains(CustomizableUI.WIDE_PANEL_CLASS)) { - return this._firstInRow(closest); - } - let targetBounds = this._lazyStoreGet(closest); - let farSide = this._dir == "ltr" ? "right" : "left"; - let outsideX = targetBounds[farSide]; - // Check if we're closer to the next target than to this one: - // Only move if we're not targeting a node in a different row: - if (aY > targetBounds.top && aY < targetBounds.bottom) { - if ((this._dir == "ltr" && aX > outsideX) || - (this._dir == "rtl" && aX < outsideX)) { - return closest.nextSibling || aContainer; - } - } - } - return closest; - }, - - /** - * "Insert" a "placeholder" by shifting the subsequent children out of the - * way. We go through all the children, and shift them based on the position - * they would have if we had inserted something before aBefore. We use CSS - * transforms for this, which are CSS transitioned. - */ - insertPlaceholder: function(aContainer, aBefore, aWide, aSize, aIsFromThisArea) { - let isShifted = false; - let shiftDown = aWide; - for (let child of aContainer.children) { - // Don't need to shift hidden nodes: - if (child.getAttribute("hidden") == "true") { - continue; - } - // If this is the node before which we're inserting, start shifting - // everything that comes after. One exception is inserting at the end - // of the menupanel, in which case we do not shift the placeholders: - if (child == aBefore && !child.classList.contains(kPlaceholderClass)) { - isShifted = true; - // If the node before which we're inserting is wide, we should - // shift everything one row down: - if (!shiftDown && this.isWide(child)) { - shiftDown = true; - } - } - // If we're moving items before a wide node that were already there, - // it's possible it's not necessary to shift nodes - // including & after the wide node. - if (this.__undoShift) { - isShifted = false; - } - if (isShifted) { - // Conversely, if we're adding something before a wide node, for - // simplicity's sake we move everything including the wide node down: - if (this.__moveDown) { - shiftDown = true; - } - if (aIsFromThisArea && !this._lastPlaceholderInsertion) { - child.setAttribute("notransition", "true"); - } - // Determine the CSS transform based on the next node: - child.style.transform = this._getNextPos(child, shiftDown, aSize); - } else { - // If we're not shifting this node, reset the transform - child.style.transform = ""; - } - } - if (aContainer.lastChild && aIsFromThisArea && - !this._lastPlaceholderInsertion) { - // Flush layout: - aContainer.lastChild.getBoundingClientRect(); - // then remove all the [notransition] - for (let child of aContainer.children) { - child.removeAttribute("notransition"); - } - } - delete this.__moveDown; - delete this.__undoShift; - this._lastPlaceholderInsertion = aBefore; - }, - - isWide: function(aNode) { - return this._wideCache.has(aNode.id); - }, - - _checkIfWide: function(aNode) { - return this._inPanel && aNode && aNode.firstChild && - aNode.firstChild.classList.contains(CustomizableUI.WIDE_PANEL_CLASS); - }, - - /** - * Reset all the transforms in this container, optionally without - * transitioning them. - * @param aContainer the container in which to reset transforms - * @param aNoTransition if truthy, adds a notransition attribute to the node - * while resetting the transform. - */ - clearPlaceholders: function(aContainer, aNoTransition) { - for (let child of aContainer.children) { - if (aNoTransition) { - child.setAttribute("notransition", true); - } - child.style.transform = ""; - if (aNoTransition) { - // Need to force a reflow otherwise this won't work. - child.getBoundingClientRect(); - child.removeAttribute("notransition"); - } - } - // We snapped back, so we can assume there's no more - // "last" placeholder insertion point to keep track of. - if (aNoTransition) { - this._lastPlaceholderInsertion = null; - } - }, - - _getNextPos: function(aNode, aShiftDown, aSize) { - // Shifting down is easy: - if (this._inPanel && aShiftDown) { - return "translate(0, " + aSize.height + "px)"; - } - return this._diffWithNext(aNode, aSize); - }, - - _diffWithNext: function(aNode, aSize) { - let xDiff; - let yDiff = null; - let nodeBounds = this._lazyStoreGet(aNode); - let side = this._dir == "ltr" ? "left" : "right"; - let next = this._getVisibleSiblingForDirection(aNode, "next"); - // First we determine the transform along the x axis. - // Usually, there will be a next node to base this on: - if (next) { - let otherBounds = this._lazyStoreGet(next); - xDiff = otherBounds[side] - nodeBounds[side]; - // If the next node is a wide item in the panel, check if we could maybe - // just move further out in the same row, without snapping to the next - // one. This happens, for example, if moving an item that's before a wide - // node within its own row of items. There will be space to drop this - // item within the row, and the rest of the items do not need to shift. - if (this.isWide(next)) { - let otherXDiff = this._moveNextBasedOnPrevious(aNode, nodeBounds, - this._firstInRow(aNode)); - // If this has the same sign as our original shift, we're still - // snapping to the start of the row. In this case, we should move - // everything after us a row down, so as not to display two nodes on - // top of each other: - // (we would be able to get away with checking for equality instead of - // equal signs here, but one of these is based on the x coordinate of - // the first item in row N and one on that for row N - 1, so this is - // safer, as their margins might differ) - if ((otherXDiff < 0) == (xDiff < 0)) { - this.__moveDown = true; - } else { - // Otherwise, we succeeded and can move further out. This also means - // we can stop shifting the rest of the content: - xDiff = otherXDiff; - this.__undoShift = true; - } - } else { - // We set this explicitly because otherwise some strange difference - // between the height and the actual difference between line creeps in - // and messes with alignments - yDiff = otherBounds.top - nodeBounds.top; - } - } else { - // We don't have a sibling whose position we can use. First, let's see - // if we're also the first item (which complicates things): - let firstNode = this._firstInRow(aNode); - if (aNode == firstNode) { - // Maybe we stored the horizontal distance between non-wide nodes, - // if not, we'll use the width of the incoming node as a proxy: - xDiff = this._horizontalDistance || aSize.width; - } else { - // If not, we should be able to get the distance to the previous node - // and use the inverse, unless there's no room for another node (ie we - // are the last node and there's no room for another one) - xDiff = this._moveNextBasedOnPrevious(aNode, nodeBounds, firstNode); - } - } - - // If we've not determined the vertical difference yet, check it here - if (yDiff === null) { - // If the next node is behind rather than in front, we must have moved - // vertically: - if ((xDiff > 0 && this._dir == "rtl") || (xDiff < 0 && this._dir == "ltr")) { - yDiff = aSize.height; - } else { - // Otherwise, we haven't - yDiff = 0; - } - } - return "translate(" + xDiff + "px, " + yDiff + "px)"; - }, - - /** - * Helper function to find the transform a node if there isn't a next node - * to base that on. - * @param aNode the node to transform - * @param aNodeBounds the bounding rect info of this node - * @param aFirstNodeInRow the first node in aNode's row - */ - _moveNextBasedOnPrevious: function(aNode, aNodeBounds, aFirstNodeInRow) { - let next = this._getVisibleSiblingForDirection(aNode, "previous"); - let otherBounds = this._lazyStoreGet(next); - let side = this._dir == "ltr" ? "left" : "right"; - let xDiff = aNodeBounds[side] - otherBounds[side]; - // If, however, this means we move outside the container's box - // (i.e. the row in which this item is placed is full) - // we should move it to align with the first item in the next row instead - let bound = this._containerInfo[this._dir == "ltr" ? "right" : "left"]; - if ((this._dir == "ltr" && xDiff + aNodeBounds.right > bound) || - (this._dir == "rtl" && xDiff + aNodeBounds.left < bound)) { - xDiff = this._lazyStoreGet(aFirstNodeInRow)[side] - aNodeBounds[side]; - } - return xDiff; - }, - - /** - * Get position details from our cache. If the node is not yet cached, get its position - * information and cache it now. - * @param aNode the node whose position info we want - * @return the position info - */ - _lazyStoreGet: function(aNode) { - let rect = this._nodePositionStore.get(aNode); - if (!rect) { - // getBoundingClientRect() returns a DOMRect that is live, meaning that - // as the element moves around, the rects values change. We don't want - // that - we want a snapshot of what the rect values are right at this - // moment, and nothing else. So we have to clone the values. - let clientRect = aNode.getBoundingClientRect(); - rect = { - left: clientRect.left, - right: clientRect.right, - width: clientRect.width, - height: clientRect.height, - top: clientRect.top, - bottom: clientRect.bottom, - }; - rect.x = rect.left + rect.width / 2; - rect.y = rect.top + rect.height / 2; - Object.freeze(rect); - this._nodePositionStore.set(aNode, rect); - } - return rect; - }, - - _firstInRow: function(aNode) { - // XXXmconley: I'm not entirely sure why we need to take the floor of these - // values - it looks like, periodically, we're getting fractional pixels back - // from lazyStoreGet. I've filed bug 994247 to investigate. - let bound = Math.floor(this._lazyStoreGet(aNode).top); - let rv = aNode; - let prev; - while (rv && (prev = this._getVisibleSiblingForDirection(rv, "previous"))) { - if (Math.floor(this._lazyStoreGet(prev).bottom) <= bound) { - return rv; - } - rv = prev; - } - return rv; - }, - - _getVisibleSiblingForDirection: function(aNode, aDirection) { - let rv = aNode; - do { - rv = rv[aDirection + "Sibling"]; - } while (rv && rv.getAttribute("hidden") == "true") - return rv; - } -} - -var DragPositionManager = { - start: function(aWindow) { - let areas = CustomizableUI.areas.filter((area) => CustomizableUI.getAreaType(area) != "toolbar"); - areas = areas.map((area) => CustomizableUI.getCustomizeTargetForArea(area, aWindow)); - areas.push(aWindow.document.getElementById(kPaletteId)); - for (let areaNode of areas) { - let positionManager = gManagers.get(areaNode); - if (positionManager) { - positionManager.update(areaNode); - } else { - gManagers.set(areaNode, new AreaPositionManager(areaNode)); - } - } - }, - - add: function(aWindow, aArea, aContainer) { - if (CustomizableUI.getAreaType(aArea) != "toolbar") { - return; - } - - gManagers.set(aContainer, new AreaPositionManager(aContainer)); - }, - - remove: function(aWindow, aArea, aContainer) { - if (CustomizableUI.getAreaType(aArea) != "toolbar") { - return; - } - - gManagers.delete(aContainer); - }, - - stop: function() { - gManagers = new WeakMap(); - }, - - getManagerForArea: function(aArea) { - return gManagers.get(aArea); - } -}; - -Object.freeze(DragPositionManager); diff --git a/browser/components/customizableui/PanelWideWidgetTracker.jsm b/browser/components/customizableui/PanelWideWidgetTracker.jsm deleted file mode 100644 index 768cebbca..000000000 --- a/browser/components/customizableui/PanelWideWidgetTracker.jsm +++ /dev/null @@ -1,172 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -this.EXPORTED_SYMBOLS = ["PanelWideWidgetTracker"]; - -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", - "resource:///modules/CustomizableUI.jsm"); - -var gPanel = CustomizableUI.AREA_PANEL; -// We keep track of the widget placements for the panel locally: -var gPanelPlacements = []; - -// All the wide widgets we know of: -var gWideWidgets = new Set(); -// All the widgets we know of: -var gSeenWidgets = new Set(); - -var PanelWideWidgetTracker = { - // Listeners used to validate panel contents whenever they change: - onWidgetAdded: function(aWidgetId, aArea, aPosition) { - if (aArea == gPanel) { - gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel); - let moveForward = this.shouldMoveForward(aWidgetId, aPosition); - this.adjustWidgets(aWidgetId, moveForward); - } - }, - onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) { - if (aArea == gPanel) { - gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel); - let moveForward = this.shouldMoveForward(aWidgetId, aNewPosition); - this.adjustWidgets(aWidgetId, moveForward); - } - }, - onWidgetRemoved: function(aWidgetId, aPrevArea) { - if (aPrevArea == gPanel) { - gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel); - this.adjustWidgets(aWidgetId, false); - } - }, - onWidgetReset: function(aWidgetId) { - gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel); - }, - // Listener to keep abreast of any new nodes. We use the DOM one because - // we need access to the actual node's classlist, so we can't use the ones above. - // Furthermore, onWidgetCreated only fires for API-based widgets, not for XUL ones. - onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer) { - if (!gSeenWidgets.has(aNode.id)) { - if (aNode.classList.contains(CustomizableUI.WIDE_PANEL_CLASS)) { - gWideWidgets.add(aNode.id); - } - gSeenWidgets.add(aNode.id); - } - }, - // When widgets get destroyed, we remove them from our sets of stuff we care about: - onWidgetDestroyed: function(aWidgetId) { - gSeenWidgets.delete(aWidgetId); - gWideWidgets.delete(aWidgetId); - }, - shouldMoveForward: function(aWidgetId, aPosition) { - let currentWidgetAtPosition = gPanelPlacements[aPosition + 1]; - let rv = gWideWidgets.has(currentWidgetAtPosition) && !gWideWidgets.has(aWidgetId); - // We might now think we can move forward, but for that we need at least 2 more small - // widgets to be present: - if (rv) { - let furtherWidgets = gPanelPlacements.slice(aPosition + 2); - let realWidgets = 0; - if (furtherWidgets.length >= 2) { - while (furtherWidgets.length && realWidgets < 2) { - let w = furtherWidgets.shift(); - if (!gWideWidgets.has(w) && this.checkWidgetStatus(w)) { - realWidgets++; - } - } - } - if (realWidgets < 2) { - rv = false; - } - } - return rv; - }, - adjustWidgets: function(aWidgetId, aMoveForwards) { - if (this.adjusting) { - return; - } - this.adjusting = true; - let widgetsAffected = gPanelPlacements.filter((w) => gWideWidgets.has(w)); - // If we're moving the wide widgets forwards (down/to the right in the panel) - // we want to start with the last widgets. Otherwise we move widgets over other wide - // widgets, which might mess up their order. Likewise, if moving backwards we should start with - // the first widget and work our way down/right from there. - let compareFn = aMoveForwards ? ((a, b) => a < b) : ((a, b) => a > b); - widgetsAffected.sort((a, b) => compareFn(gPanelPlacements.indexOf(a), - gPanelPlacements.indexOf(b))); - for (let widget of widgetsAffected) { - this.adjustPosition(widget, aMoveForwards); - } - this.adjusting = false; - }, - // This function is called whenever an item gets moved in the menu panel. It - // adjusts the position of widgets within the panel to prevent "gaps" between - // wide widgets that could be filled up with single column widgets - adjustPosition: function(aWidgetId, aMoveForwards) { - // Make sure that there are n % columns = 0 narrow buttons before the widget. - let placementIndex = gPanelPlacements.indexOf(aWidgetId); - let prevSiblingCount = 0; - let fixedPos = null; - while (placementIndex--) { - let thisWidgetId = gPanelPlacements[placementIndex]; - if (gWideWidgets.has(thisWidgetId)) { - continue; - } - let widgetStatus = this.checkWidgetStatus(thisWidgetId); - if (!widgetStatus) { - continue; - } - if (widgetStatus == "public-only") { - fixedPos = !fixedPos ? placementIndex : Math.min(fixedPos, placementIndex); - prevSiblingCount = 0; - } else { - prevSiblingCount++; - } - } - - if (fixedPos !== null || prevSiblingCount % CustomizableUI.PANEL_COLUMN_COUNT) { - let desiredPos = (fixedPos !== null) ? fixedPos : gPanelPlacements.indexOf(aWidgetId); - let desiredChange = -(prevSiblingCount % CustomizableUI.PANEL_COLUMN_COUNT); - if (aMoveForwards && fixedPos == null) { - // +1 because otherwise we'd count ourselves: - desiredChange = CustomizableUI.PANEL_COLUMN_COUNT + desiredChange + 1; - } - desiredPos += desiredChange; - CustomizableUI.moveWidgetWithinArea(aWidgetId, desiredPos); - } - }, - - /* - * Check whether a widget id is actually known anywhere. - * @returns false if the widget doesn't exist, - * "public-only" if it's not shown in private windows - * "real" if it does exist and is shown even in private windows - */ - checkWidgetStatus: function(aWidgetId) { - let widgetWrapper = CustomizableUI.getWidget(aWidgetId); - // This widget might not actually exist: - if (!widgetWrapper) { - return false; - } - // This widget might still not actually exist: - if (widgetWrapper.provider == CustomizableUI.PROVIDER_XUL && - widgetWrapper.instances.length == 0) { - return false; - } - - // Or it might only be there some of the time: - if (widgetWrapper.provider == CustomizableUI.PROVIDER_API && - widgetWrapper.showInPrivateBrowsing === false) { - return "public-only"; - } - return "real"; - }, - - init: function() { - // Initialize our local placements copy and register the listener - gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel); - CustomizableUI.addListener(this); - }, -}; diff --git a/browser/components/customizableui/ScrollbarSampler.jsm b/browser/components/customizableui/ScrollbarSampler.jsm deleted file mode 100644 index 44736e4c4..000000000 --- a/browser/components/customizableui/ScrollbarSampler.jsm +++ /dev/null @@ -1,65 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -"use strict"; - -this.EXPORTED_SYMBOLS = ["ScrollbarSampler"]; - -const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components; - -Cu.import("resource://gre/modules/Services.jsm"); -Cu.import("resource://gre/modules/XPCOMUtils.jsm"); - -var gSystemScrollbarWidth = null; - -this.ScrollbarSampler = { - getSystemScrollbarWidth: function() { - if (gSystemScrollbarWidth !== null) { - return Promise.resolve(gSystemScrollbarWidth); - } - - return new Promise(resolve => { - this._sampleSystemScrollbarWidth().then(function(systemScrollbarWidth) { - gSystemScrollbarWidth = systemScrollbarWidth; - resolve(gSystemScrollbarWidth); - }); - }); - }, - - resetSystemScrollbarWidth: function() { - gSystemScrollbarWidth = null; - }, - - _sampleSystemScrollbarWidth: function() { - let hwin = Services.appShell.hiddenDOMWindow; - let hdoc = hwin.document.documentElement; - let iframe = hwin.document.createElementNS("http://www.w3.org/1999/xhtml", - "html:iframe"); - iframe.setAttribute("srcdoc", '<body style="overflow-y: scroll"></body>'); - hdoc.appendChild(iframe); - - let cwindow = iframe.contentWindow; - let utils = cwindow.QueryInterface(Ci.nsIInterfaceRequestor) - .getInterface(Ci.nsIDOMWindowUtils); - - return new Promise(resolve => { - cwindow.addEventListener("load", function onLoad(aEvent) { - cwindow.removeEventListener("load", onLoad); - let sbWidth = {}; - try { - utils.getScrollbarSize(true, sbWidth, {}); - } catch (e) { - Cu.reportError("Could not sample scrollbar size: " + e + " -- " + - e.stack); - sbWidth.value = 0; - } - // Minimum width of 10 so that we have enough padding: - sbWidth.value = Math.max(sbWidth.value, 10); - resolve(sbWidth.value); - iframe.remove(); - }); - }); - } -}; -Object.freeze(this.ScrollbarSampler); diff --git a/browser/components/customizableui/content/customizeMode.inc.xul b/browser/components/customizableui/content/customizeMode.inc.xul deleted file mode 100644 index b665630a2..000000000 --- a/browser/components/customizableui/content/customizeMode.inc.xul +++ /dev/null @@ -1,82 +0,0 @@ -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - -<hbox id="customization-container" flex="1" hidden="true"> - <vbox flex="1" id="customization-palette-container"> - <label id="customization-header"> - &customizeMode.menuAndToolbars.header2; - </label> - <hbox id="customization-empty" hidden="true"> - <label>&customizeMode.menuAndToolbars.empty;</label> - <label onclick="BrowserOpenAddonsMgr('addons://discover/');" - onkeypress="BrowserOpenAddonsMgr('addons://discover/');" - id="customization-more-tools" - class="text-link"> - &customizeMode.menuAndToolbars.emptyLink; - </label> - </hbox> - <vbox id="customization-palette" class="customization-palette"/> - <spacer id="customization-spacer"/> - <hbox id="customization-footer"> -#ifdef CAN_DRAW_IN_TITLEBAR - <button id="customization-titlebar-visibility-button" class="customizationmode-button" - label="&customizeMode.titlebar;" type="checkbox" -#NB: because oncommand fires after click, by the time we've fired, the checkbox binding -# will already have switched the button's state, so this is correct: - oncommand="gCustomizeMode.toggleTitlebar(this.hasAttribute('checked'))"/> -#endif - <button id="customization-toolbar-visibility-button" label="&customizeMode.toolbars;" class="customizationmode-button" type="menu"> - <menupopup id="customization-toolbar-menu" onpopupshowing="onViewToolbarsPopupShowing(event)"/> - </button> - <button id="customization-lwtheme-button" label="&customizeMode.lwthemes;" class="customizationmode-button" type="menu"> - <panel type="arrow" id="customization-lwtheme-menu" - onpopupshowing="gCustomizeMode.onLWThemesMenuShowing(event);" - position="topcenter bottomleft" - flip="none" - role="menu"> - <label id="customization-lwtheme-menu-header" value="&customizeMode.lwthemes.myThemes;"/> - <label id="customization-lwtheme-menu-recommended" value="&customizeMode.lwthemes.recommended;"/> - <hbox id="customization-lwtheme-menu-footer"> - <toolbarbutton class="customization-lwtheme-menu-footeritem" - label="&customizeMode.lwthemes.menuManage;" - accesskey="&customizeMode.lwthemes.menuManage.accessKey;" - tabindex="0" - oncommand="gCustomizeMode.openAddonsManagerThemes(event);"/> - <toolbarbutton class="customization-lwtheme-menu-footeritem" - label="&customizeMode.lwthemes.menuGetMore;" - accesskey="&customizeMode.lwthemes.menuGetMore.accessKey;" - tabindex="0" - oncommand="gCustomizeMode.getMoreThemes(event);"/> - </hbox> - </panel> - </button> - - <spacer id="customization-footer-spacer"/> - <button id="customization-undo-reset-button" - class="customizationmode-button" - hidden="true" - oncommand="gCustomizeMode.undoReset();" - label="&undoCmd.label;"/> - <button id="customization-reset-button" - oncommand="gCustomizeMode.reset();" - label="&customizeMode.restoreDefaults;" - class="customizationmode-button"/> - </hbox> - </vbox> - <vbox id="customization-panel-container"> - <vbox id="customization-panelWrapper"> - <html:style html:type="text/html" scoped="scoped"> - @import url(chrome://global/skin/popup.css); - </html:style> - <box class="panel-arrowbox"> - <box flex="1"/> - <image class="panel-arrow" side="top"/> - </box> - <box class="panel-arrowcontent" side="top" flex="1"> - <hbox id="customization-panelHolder"/> - <box class="panel-inner-arrowcontentfooter" hidden="true"/> - </box> - </vbox> - </vbox> -</hbox> diff --git a/browser/components/customizableui/content/jar.mn b/browser/components/customizableui/content/jar.mn deleted file mode 100644 index 05c0112cd..000000000 --- a/browser/components/customizableui/content/jar.mn +++ /dev/null @@ -1,10 +0,0 @@ -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -browser.jar: - content/browser/customizableui/panelUI.css - content/browser/customizableui/panelUI.js - content/browser/customizableui/panelUI.xml - content/browser/customizableui/toolbar.xml - diff --git a/browser/components/customizableui/content/moz.build b/browser/components/customizableui/content/moz.build deleted file mode 100644 index eb4454d28..000000000 --- a/browser/components/customizableui/content/moz.build +++ /dev/null @@ -1,7 +0,0 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -JAR_MANIFESTS += ['jar.mn']
\ No newline at end of file diff --git a/browser/components/customizableui/content/panelUI.css b/browser/components/customizableui/content/panelUI.css deleted file mode 100644 index ba44636f1..000000000 --- a/browser/components/customizableui/content/panelUI.css +++ /dev/null @@ -1,31 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this - * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ - -.panel-viewstack[viewtype="main"] > .panel-clickcapturer { - pointer-events: none; -} - -.panel-mainview, -.panel-viewcontainer, -.panel-viewstack { - overflow: hidden; -} - -.panel-viewstack { - position: relative; -} - -.panel-subviews { - -moz-stack-sizing: ignore; - transform: translateX(0); - overflow-y: auto; -} - -.panel-subviews[panelopen] { - transition: transform var(--panelui-subview-transition-duration); -} - -.panel-viewcontainer[panelopen]:-moz-any(:not([viewtype="main"]),[transitioning="true"]) { - transition: height var(--panelui-subview-transition-duration); -} diff --git a/browser/components/customizableui/content/panelUI.inc.xul b/browser/components/customizableui/content/panelUI.inc.xul deleted file mode 100644 index e389b14bf..000000000 --- a/browser/components/customizableui/content/panelUI.inc.xul +++ /dev/null @@ -1,386 +0,0 @@ -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - -<panel id="PanelUI-popup" - role="group" - type="arrow" - hidden="true" - flip="slide" - position="bottomcenter topright" - noautofocus="true"> - <panelmultiview id="PanelUI-multiView" mainViewId="PanelUI-mainView"> - <panelview id="PanelUI-mainView" context="customizationPanelContextMenu"> - <vbox id="PanelUI-contents-scroller"> - <vbox id="PanelUI-contents" class="panelUI-grid"/> - </vbox> - - <footer id="PanelUI-footer"> - <toolbarbutton id="PanelUI-update-status" - oncommand="gMenuButtonUpdateBadge.onMenuPanelCommand(event);" - wrap="true" - hidden="true"/> - <hbox id="PanelUI-footer-fxa"> - <hbox id="PanelUI-fxa-status" - defaultlabel="&fxaSignIn.label;" - signedinTooltiptext="&fxaSignedIn.tooltip;" - tooltiptext="&fxaSignedIn.tooltip;" - errorlabel="&fxaSignInError.label;" - unverifiedlabel="&fxaUnverified.label;" - onclick="if (event.which == 1) gFxAccounts.onMenuPanelCommand();"> - <image id="PanelUI-fxa-avatar"/> - <toolbarbutton id="PanelUI-fxa-label" - fxabrandname="&syncBrand.fxAccount.label;"/> - </hbox> - <toolbarseparator/> - <toolbarbutton id="PanelUI-fxa-icon" - oncommand="gSyncUI.doSync();" - closemenu="none"> - <observes element="sync-status" attribute="syncstatus"/> - <observes element="sync-status" attribute="tooltiptext"/> - </toolbarbutton> - </hbox> - - <hbox id="PanelUI-footer-inner"> - <toolbarbutton id="PanelUI-customize" label="&appMenuCustomize.label;" - exitLabel="&appMenuCustomizeExit.label;" - tooltiptext="&appMenuCustomize.tooltip;" - exitTooltiptext="&appMenuCustomizeExit.tooltip;" - closemenu="none" - oncommand="gCustomizeMode.toggle();"/> - <toolbarseparator/> - <toolbarbutton id="PanelUI-help" label="&helpMenu.label;" - closemenu="none" - tooltiptext="&appMenuHelp.tooltip;" - oncommand="PanelUI.showHelpView(this);"/> - <toolbarseparator/> - <toolbarbutton id="PanelUI-quit" -#ifdef XP_WIN - label="&quitApplicationCmdWin2.label;" - tooltiptext="&quitApplicationCmdWin2.tooltip;" -#else -#ifdef XP_MACOSX - label="&quitApplicationCmdMac2.label;" -#else - label="&quitApplicationCmd.label;" -#endif -#endif - command="cmd_quitApplication"/> - </hbox> - </footer> - </panelview> - - <panelview id="PanelUI-history" flex="1"> - <label value="&appMenuHistory.label;" class="panel-subview-header"/> - <vbox class="panel-subview-body"> - <toolbarbutton id="appMenuViewHistorySidebar" - label="&appMenuHistory.viewSidebar.label;" - type="checkbox" - class="subviewbutton" - key="key_gotoHistory" - oncommand="SidebarUI.toggle('viewHistorySidebar'); PanelUI.hide();"> - <observes element="viewHistorySidebar" attribute="checked"/> - </toolbarbutton> - <toolbarbutton id="appMenuClearRecentHistory" - label="&appMenuHistory.clearRecent.label;" - class="subviewbutton" - command="Tools:Sanitize"/> - <toolbarbutton id="appMenuRestoreLastSession" - label="&appMenuHistory.restoreSession.label;" - class="subviewbutton" - command="Browser:RestoreLastSession"/> - <menuseparator id="PanelUI-recentlyClosedTabs-separator"/> - <vbox id="PanelUI-recentlyClosedTabs" tooltip="bhTooltip"/> - <menuseparator id="PanelUI-recentlyClosedWindows-separator"/> - <vbox id="PanelUI-recentlyClosedWindows" tooltip="bhTooltip"/> - <menuseparator id="PanelUI-historyItems-separator"/> - <vbox id="PanelUI-historyItems" tooltip="bhTooltip"/> - </vbox> - <toolbarbutton id="PanelUI-historyMore" - class="panel-subview-footer subviewbutton" - label="&appMenuHistory.showAll.label;" - oncommand="PlacesCommandHook.showPlacesOrganizer('History'); CustomizableUI.hidePanelForNode(this);"/> - </panelview> - - <panelview id="PanelUI-remotetabs" flex="1" class="PanelUI-subView"> - <label value="&appMenuRemoteTabs.label;" class="panel-subview-header"/> - <vbox class="panel-subview-body"> - <!-- this widget has 3 boxes in the body, but only 1 is ever visible --> - <!-- When Sync is ready to sync --> - <vbox id="PanelUI-remotetabs-main" observes="sync-syncnow-state"> - <vbox id="PanelUI-remotetabs-buttons"> - <toolbarbutton id="PanelUI-remotetabs-view-sidebar" - class="subviewbutton" - observes="viewTabsSidebar" - label="&appMenuRemoteTabs.sidebar.label;"/> - <toolbarbutton id="PanelUI-remotetabs-syncnow" - observes="sync-status" - class="subviewbutton" - oncommand="gSyncUI.doSync();" - closemenu="none"/> - <menuseparator id="PanelUI-remotetabs-separator"/> - </vbox> - <deck id="PanelUI-remotetabs-deck"> - <!-- Sync is ready to Sync and the "tabs" engine is enabled --> - <vbox id="PanelUI-remotetabs-tabspane"> - <vbox id="PanelUI-remotetabs-tabslist" - notabsforclientlabel="&appMenuRemoteTabs.notabs.label;" - /> - </vbox> - <!-- Sync is ready to Sync but the "tabs" engine isn't enabled--> - <hbox id="PanelUI-remotetabs-tabsdisabledpane" pack="center" flex="1"> - <vbox class="PanelUI-remotetabs-instruction-box"> - <hbox pack="center"> - <image class="fxaSyncIllustration" alt=""/> - </hbox> - <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.tabsnotsyncing.label;</label> - <hbox pack="center"> - <toolbarbutton class="PanelUI-remotetabs-prefs-button" - label="&appMenuRemoteTabs.openprefs.label;" - oncommand="gSyncUI.openSetup(null, 'synced-tabs');"/> - </hbox> - </vbox> - </hbox> - <!-- Sync is ready to Sync but we are still fetching the tabs to show --> - <vbox id="PanelUI-remotetabs-fetching"> - <!-- Show intentionally blank panel, see bug 1239845 --> - </vbox> - <!-- Sync has only 1 (ie, this) device connected --> - <hbox id="PanelUI-remotetabs-nodevicespane" pack="center" flex="1"> - <vbox class="PanelUI-remotetabs-instruction-box"> - <hbox pack="center"> - <image class="fxaSyncIllustration" alt=""/> - </hbox> - <label class="PanelUI-remotetabs-instruction-title">&appMenuRemoteTabs.noclients.title;</label> - <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.noclients.subtitle;</label> - <!-- The inner HTML for PanelUI-remotetabs-mobile-promo is built at runtime --> - <label id="PanelUI-remotetabs-mobile-promo" fxAccountsBrand="&syncBrand.fxAccount.label;"/> - </vbox> - </hbox> - </deck> - </vbox> - <!-- a box to ensure contained boxes are centered horizonally --> - <hbox pack="center" flex="1"> - <!-- When Sync is not configured --> - <vbox id="PanelUI-remotetabs-setupsync" - flex="1" - align="center" - class="PanelUI-remotetabs-instruction-box" - observes="sync-setup-state"> - <image class="fxaSyncIllustration" alt=""/> - <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.notsignedin.label;</label> - <toolbarbutton class="PanelUI-remotetabs-prefs-button" - label="&appMenuRemoteTabs.signin.label;" - oncommand="gSyncUI.openSetup(null, 'synced-tabs');"/> - </vbox> - <!-- When Sync needs re-authentication. This uses the exact same messaging - as "Sync is not configured" but remains a separate box so we get - the goodness of observing broadcasters to manage the hidden states --> - <vbox id="PanelUI-remotetabs-reauthsync" - flex="1" - align="center" - class="PanelUI-remotetabs-instruction-box" - observes="sync-reauth-state"> - <image class="fxaSyncIllustration" alt=""/> - <label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.notsignedin.label;</label> - <toolbarbutton class="PanelUI-remotetabs-prefs-button" - label="&appMenuRemoteTabs.signin.label;" - oncommand="gSyncUI.openSetup(null, 'synced-tabs');"/> - </vbox> - </hbox> - </vbox> - </panelview> - - <panelview id="PanelUI-bookmarks" flex="1" class="PanelUI-subView"> - <label value="&bookmarksMenu.label;" class="panel-subview-header"/> - <vbox class="panel-subview-body"> - <toolbarbutton id="panelMenuBookmarkThisPage" - class="subviewbutton" - observes="bookmarkThisPageBroadcaster" - command="Browser:AddBookmarkAs" - onclick="PanelUI.hide();"/> - <toolbarseparator/> - <toolbarbutton id="panelMenu_viewBookmarksSidebar" - label="&viewBookmarksSidebar2.label;" - class="subviewbutton" - key="viewBookmarksSidebarKb" - oncommand="SidebarUI.toggle('viewBookmarksSidebar'); PanelUI.hide();"> - <observes element="viewBookmarksSidebar" attribute="checked"/> - </toolbarbutton> - <toolbarbutton id="panelMenu_viewBookmarksToolbar" - label="&viewBookmarksToolbar.label;" - type="checkbox" - toolbarId="PersonalToolbar" - class="subviewbutton" - oncommand="onViewToolbarCommand(event); PanelUI.hide();"/> - <toolbarseparator/> - <toolbarbutton id="panelMenu_bookmarksToolbar" - label="&personalbarCmd.label;" - class="subviewbutton cui-withicon" - oncommand="PlacesCommandHook.showPlacesOrganizer('BookmarksToolbar'); PanelUI.hide();"/> - <toolbarbutton id="panelMenu_unsortedBookmarks" - label="&otherBookmarksCmd.label;" - class="subviewbutton cui-withicon" - oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks'); PanelUI.hide();"/> - <toolbarseparator class="small-separator"/> - <toolbaritem id="panelMenu_bookmarksMenu" - orient="vertical" - smoothscroll="false" - onclick="if (event.button == 1) BookmarkingUI.onPanelMenuViewCommand(event, this._placesView);" - oncommand="BookmarkingUI.onPanelMenuViewCommand(event, this._placesView);" - flatList="true" - tooltip="bhTooltip"> - <!-- bookmarks menu items will go here --> - </toolbaritem> - </vbox> - <toolbarbutton id="panelMenu_showAllBookmarks" - label="&showAllBookmarks2.label;" - class="subviewbutton panel-subview-footer" - command="Browser:ShowAllBookmarks" - onclick="PanelUI.hide();"/> - </panelview> - - <panelview id="PanelUI-feeds" flex="1" oncommand="FeedHandler.subscribeToFeed(null, event);"> - <label value="&feedsMenu2.label;" class="panel-subview-header"/> - </panelview> - - <panelview id="PanelUI-containers" flex="1"> - <label value="&containersMenu.label;" class="panel-subview-header"/> - <vbox id="PanelUI-containersItems"/> - </panelview> - - <panelview id="PanelUI-helpView" flex="1" class="PanelUI-subView"> - <label value="&helpMenu.label;" class="panel-subview-header"/> - <vbox id="PanelUI-helpItems" class="panel-subview-body"/> - </panelview> - - <panelview id="PanelUI-developer" flex="1"> - <label value="&webDeveloperMenu.label;" class="panel-subview-header"/> - <vbox id="PanelUI-developerItems" class="panel-subview-body"/> - </panelview> - - <panelview id="PanelUI-sidebar" flex="1"> - <label value="&appMenuSidebars.label;" class="panel-subview-header"/> - <vbox id="PanelUI-sidebarItems" class="panel-subview-body"/> - </panelview> - - <panelview id="PanelUI-characterEncodingView" flex="1"> - <label value="&charsetMenu2.label;" class="panel-subview-header"/> - <vbox class="panel-subview-body"> - <vbox id="PanelUI-characterEncodingView-pinned" - class="PanelUI-characterEncodingView-list"/> - <toolbarseparator/> - <vbox id="PanelUI-characterEncodingView-charsets" - class="PanelUI-characterEncodingView-list"/> - <toolbarseparator/> - <vbox> - <label id="PanelUI-characterEncodingView-autodetect-label"/> - <vbox id="PanelUI-characterEncodingView-autodetect" - class="PanelUI-characterEncodingView-list"/> - </vbox> - </vbox> - </panelview> - - <panelview id="PanelUI-panicView" flex="1"> - <vbox class="panel-subview-body"> - <hbox id="PanelUI-panic-timeframe"> - <image id="PanelUI-panic-timeframe-icon" alt=""/> - <vbox flex="1"> - <hbox id="PanelUI-panic-header"> - <image id="PanelUI-panic-timeframe-icon-small" alt=""/> - <description id="PanelUI-panic-mainDesc" flex="1">&panicButton.view.mainTimeframeDesc;</description> - </hbox> - <radiogroup id="PanelUI-panic-timeSpan" aria-labelledby="PanelUI-panic-mainDesc" closemenu="none"> - <radio id="PanelUI-panic-5min" label="&panicButton.view.5min;" selected="true" - value="5" class="subviewradio"/> - <radio id="PanelUI-panic-2hr" label="&panicButton.view.2hr;" - value="2" class="subviewradio"/> - <radio id="PanelUI-panic-day" label="&panicButton.view.day;" - value="6" class="subviewradio"/> - </radiogroup> - </vbox> - </hbox> - <vbox id="PanelUI-panic-explanations"> - <label id="PanelUI-panic-actionlist-main-label">&panicButton.view.mainActionDesc;</label> - - <label id="PanelUI-panic-actionlist-windows" class="PanelUI-panic-actionlist">&panicButton.view.deleteTabsAndWindows;</label> - <label id="PanelUI-panic-actionlist-cookies" class="PanelUI-panic-actionlist">&panicButton.view.deleteCookies;</label> - <label id="PanelUI-panic-actionlist-history" class="PanelUI-panic-actionlist">&panicButton.view.deleteHistory;</label> - <label id="PanelUI-panic-actionlist-newwindow" class="PanelUI-panic-actionlist">&panicButton.view.openNewWindow;</label> - - <label id="PanelUI-panic-warning">&panicButton.view.undoWarning;</label> - </vbox> - <button id="PanelUI-panic-view-button" - label="&panicButton.view.forgetButton;"/> - </vbox> - </panelview> - - </panelmultiview> - <!-- These menupopups are located here to prevent flickering, - see bug 492960 comment 20. --> - <menupopup id="customizationPanelItemContextMenu"> - <menuitem oncommand="gCustomizeMode.addToToolbar(document.popupNode)" - closemenu="single" - class="customize-context-moveToToolbar" - accesskey="&customizeMenu.moveToToolbar.accesskey;" - label="&customizeMenu.moveToToolbar.label;"/> - <menuitem oncommand="gCustomizeMode.removeFromArea(document.popupNode)" - closemenu="single" - class="customize-context-removeFromPanel" - accesskey="&customizeMenu.removeFromMenu.accesskey;" - label="&customizeMenu.removeFromMenu.label;"/> - <menuseparator/> - <menuitem command="cmd_CustomizeToolbars" - class="viewCustomizeToolbar" - accesskey="&viewCustomizeToolbar.accesskey;" - label="&viewCustomizeToolbar.label;"/> - </menupopup> - - <menupopup id="customizationPaletteItemContextMenu"> - <menuitem oncommand="gCustomizeMode.addToToolbar(document.popupNode)" - class="customize-context-addToToolbar" - accesskey="&customizeMenu.addToToolbar.accesskey;" - label="&customizeMenu.addToToolbar.label;"/> - <menuitem oncommand="gCustomizeMode.addToPanel(document.popupNode)" - class="customize-context-addToPanel" - accesskey="&customizeMenu.addToPanel.accesskey;" - label="&customizeMenu.addToPanel.label;"/> - </menupopup> - - <menupopup id="customizationPanelContextMenu"> - <menuitem command="cmd_CustomizeToolbars" - accesskey="&customizeMenu.addMoreItems.accesskey;" - label="&customizeMenu.addMoreItems.label;"/> - </menupopup> -</panel> - -<panel id="widget-overflow" - role="group" - type="arrow" - noautofocus="true" - context="toolbar-context-menu" - position="bottomcenter topright" - hidden="true"> - <vbox id="widget-overflow-scroller"> - <vbox id="widget-overflow-list" class="widget-overflow-list" - overflowfortoolbar="nav-bar"/> - </vbox> -</panel> - -<panel id="panic-button-success-notification" - type="arrow" - position="bottomcenter topright" - hidden="true" - role="alert" - orient="vertical"> - <hbox id="panic-button-success-header"> - <image id="panic-button-success-icon" alt=""/> - <vbox> - <description>&panicButton.thankyou.msg1;</description> - <description>&panicButton.thankyou.msg2;</description> - </vbox> - </hbox> - <button label="&panicButton.thankyou.buttonlabel;" - id="panic-button-success-closebutton" - oncommand="PanicButtonNotifier.close()"/> -</panel> diff --git a/browser/components/customizableui/content/panelUI.js b/browser/components/customizableui/content/panelUI.js deleted file mode 100644 index 66fa0c184..000000000 --- a/browser/components/customizableui/content/panelUI.js +++ /dev/null @@ -1,558 +0,0 @@ -/* This Source Code Form is subject to the terms of the Mozilla Public - * License, v. 2.0. If a copy of the MPL was not distributed with this file, - * You can obtain one at http://mozilla.org/MPL/2.0/. */ - -XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI", - "resource:///modules/CustomizableUI.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ScrollbarSampler", - "resource:///modules/ScrollbarSampler.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils", - "resource://gre/modules/ShortcutUtils.jsm"); -XPCOMUtils.defineLazyModuleGetter(this, "AppConstants", - "resource://gre/modules/AppConstants.jsm"); - -/** - * Maintains the state and dispatches events for the main menu panel. - */ - -const PanelUI = { - /** Panel events that we listen for. **/ - get kEvents() { - return ["popupshowing", "popupshown", "popuphiding", "popuphidden"]; - }, - /** - * Used for lazily getting and memoizing elements from the document. Lazy - * getters are set in init, and memoizing happens after the first retrieval. - */ - get kElements() { - return { - contents: "PanelUI-contents", - mainView: "PanelUI-mainView", - multiView: "PanelUI-multiView", - helpView: "PanelUI-helpView", - menuButton: "PanelUI-menu-button", - panel: "PanelUI-popup", - scroller: "PanelUI-contents-scroller" - }; - }, - - _initialized: false, - init: function() { - for (let [k, v] of Object.entries(this.kElements)) { - // Need to do fresh let-bindings per iteration - let getKey = k; - let id = v; - this.__defineGetter__(getKey, function() { - delete this[getKey]; - return this[getKey] = document.getElementById(id); - }); - } - - this.menuButton.addEventListener("mousedown", this); - this.menuButton.addEventListener("keypress", this); - this._overlayScrollListenerBoundFn = this._overlayScrollListener.bind(this); - window.matchMedia("(-moz-overlay-scrollbars)").addListener(this._overlayScrollListenerBoundFn); - CustomizableUI.addListener(this); - this._initialized = true; - }, - - _eventListenersAdded: false, - _ensureEventListenersAdded: function() { - if (this._eventListenersAdded) - return; - this._addEventListeners(); - }, - - _addEventListeners: function() { - for (let event of this.kEvents) { - this.panel.addEventListener(event, this); - } - - this.helpView.addEventListener("ViewShowing", this._onHelpViewShow, false); - this._eventListenersAdded = true; - }, - - uninit: function() { - for (let event of this.kEvents) { - this.panel.removeEventListener(event, this); - } - this.helpView.removeEventListener("ViewShowing", this._onHelpViewShow); - this.menuButton.removeEventListener("mousedown", this); - this.menuButton.removeEventListener("keypress", this); - window.matchMedia("(-moz-overlay-scrollbars)").removeListener(this._overlayScrollListenerBoundFn); - CustomizableUI.removeListener(this); - this._overlayScrollListenerBoundFn = null; - }, - - /** - * Customize mode extracts the mainView and puts it somewhere else while the - * user customizes. Upon completion, this function can be called to put the - * panel back to where it belongs in normal browsing mode. - * - * @param aMainView - * The mainView node to put back into place. - */ - setMainView: function(aMainView) { - this._ensureEventListenersAdded(); - this.multiView.setMainView(aMainView); - }, - - /** - * Opens the menu panel if it's closed, or closes it if it's - * open. - * - * @param aEvent the event that triggers the toggle. - */ - toggle: function(aEvent) { - // Don't show the panel if the window is in customization mode, - // since this button doubles as an exit path for the user in this case. - if (document.documentElement.hasAttribute("customizing")) { - return; - } - this._ensureEventListenersAdded(); - if (this.panel.state == "open") { - this.hide(); - } else if (this.panel.state == "closed") { - this.show(aEvent); - } - }, - - /** - * Opens the menu panel. If the event target has a child with the - * toolbarbutton-icon attribute, the panel will be anchored on that child. - * Otherwise, the panel is anchored on the event target itself. - * - * @param aEvent the event (if any) that triggers showing the menu. - */ - show: function(aEvent) { - return new Promise(resolve => { - this.ensureReady().then(() => { - if (this.panel.state == "open" || - document.documentElement.hasAttribute("customizing")) { - resolve(); - return; - } - - let editControlPlacement = CustomizableUI.getPlacementOfWidget("edit-controls"); - if (editControlPlacement && editControlPlacement.area == CustomizableUI.AREA_PANEL) { - updateEditUIVisibility(); - } - - let personalBookmarksPlacement = CustomizableUI.getPlacementOfWidget("personal-bookmarks"); - if (personalBookmarksPlacement && - personalBookmarksPlacement.area == CustomizableUI.AREA_PANEL) { - PlacesToolbarHelper.customizeChange(); - } - - let anchor; - if (!aEvent || - aEvent.type == "command") { - anchor = this.menuButton; - } else { - anchor = aEvent.target; - } - - this.panel.addEventListener("popupshown", function onPopupShown() { - this.removeEventListener("popupshown", onPopupShown); - resolve(); - }); - - let iconAnchor = - document.getAnonymousElementByAttribute(anchor, "class", - "toolbarbutton-icon"); - this.panel.openPopup(iconAnchor || anchor); - }, (reason) => { - console.error("Error showing the PanelUI menu", reason); - }); - }); - }, - - /** - * If the menu panel is being shown, hide it. - */ - hide: function() { - if (document.documentElement.hasAttribute("customizing")) { - return; - } - - this.panel.hidePopup(); - }, - - handleEvent: function(aEvent) { - // Ignore context menus and menu button menus showing and hiding: - if (aEvent.type.startsWith("popup") && - aEvent.target != this.panel) { - return; - } - switch (aEvent.type) { - case "popupshowing": - this._adjustLabelsForAutoHyphens(); - // Fall through - case "popupshown": - // Fall through - case "popuphiding": - // Fall through - case "popuphidden": - this._updatePanelButton(aEvent.target); - break; - case "mousedown": - if (aEvent.button == 0) - this.toggle(aEvent); - break; - case "keypress": - this.toggle(aEvent); - break; - } - }, - - get isReady() { - return !!this._isReady; - }, - - /** - * Registering the menu panel is done lazily for performance reasons. This - * method is exposed so that CustomizationMode can force panel-readyness in the - * event that customization mode is started before the panel has been opened - * by the user. - * - * @param aCustomizing (optional) set to true if this was called while entering - * customization mode. If that's the case, we trust that customization - * mode will handle calling beginBatchUpdate and endBatchUpdate. - * - * @return a Promise that resolves once the panel is ready to roll. - */ - ensureReady: function(aCustomizing=false) { - if (this._readyPromise) { - return this._readyPromise; - } - this._readyPromise = Task.spawn(function*() { - if (!this._initialized) { - yield new Promise(resolve => { - let delayedStartupObserver = (aSubject, aTopic, aData) => { - if (aSubject == window) { - Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished"); - resolve(); - } - }; - Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false); - }); - } - - this.contents.setAttributeNS("http://www.w3.org/XML/1998/namespace", "lang", - getLocale()); - if (!this._scrollWidth) { - // In order to properly center the contents of the panel, while ensuring - // that we have enough space on either side to show a scrollbar, we have to - // do a bit of hackery. In particular, we calculate a new width for the - // scroller, based on the system scrollbar width. - this._scrollWidth = - (yield ScrollbarSampler.getSystemScrollbarWidth()) + "px"; - let cstyle = window.getComputedStyle(this.scroller); - let widthStr = cstyle.width; - // Get the calculated padding on the left and right sides of - // the scroller too. We'll use that in our final calculation so - // that if a scrollbar appears, we don't have the contents right - // up against the edge of the scroller. - let paddingLeft = cstyle.paddingLeft; - let paddingRight = cstyle.paddingRight; - let calcStr = [widthStr, this._scrollWidth, - paddingLeft, paddingRight].join(" + "); - this.scroller.style.width = "calc(" + calcStr + ")"; - } - - if (aCustomizing) { - CustomizableUI.registerMenuPanel(this.contents); - } else { - this.beginBatchUpdate(); - try { - CustomizableUI.registerMenuPanel(this.contents); - } finally { - this.endBatchUpdate(); - } - } - this._updateQuitTooltip(); - this.panel.hidden = false; - this._isReady = true; - }.bind(this)).then(null, Cu.reportError); - - return this._readyPromise; - }, - - /** - * Switch the panel to the main view if it's not already - * in that view. - */ - showMainView: function() { - this._ensureEventListenersAdded(); - this.multiView.showMainView(); - }, - - /** - * Switch the panel to the help view if it's not already - * in that view. - */ - showHelpView: function(aAnchor) { - this._ensureEventListenersAdded(); - this.multiView.showSubView("PanelUI-helpView", aAnchor); - }, - - /** - * Shows a subview in the panel with a given ID. - * - * @param aViewId the ID of the subview to show. - * @param aAnchor the element that spawned the subview. - * @param aPlacementArea the CustomizableUI area that aAnchor is in. - */ - showSubView: Task.async(function*(aViewId, aAnchor, aPlacementArea) { - this._ensureEventListenersAdded(); - let viewNode = document.getElementById(aViewId); - if (!viewNode) { - Cu.reportError("Could not show panel subview with id: " + aViewId); - return; - } - - if (!aAnchor) { - Cu.reportError("Expected an anchor when opening subview with id: " + aViewId); - return; - } - - if (aPlacementArea == CustomizableUI.AREA_PANEL) { - this.multiView.showSubView(aViewId, aAnchor); - } else if (!aAnchor.open) { - aAnchor.open = true; - - let tempPanel = document.createElement("panel"); - tempPanel.setAttribute("type", "arrow"); - tempPanel.setAttribute("id", "customizationui-widget-panel"); - tempPanel.setAttribute("class", "cui-widget-panel"); - tempPanel.setAttribute("viewId", aViewId); - if (aAnchor.getAttribute("tabspecific")) { - tempPanel.setAttribute("tabspecific", true); - } - if (this._disableAnimations) { - tempPanel.setAttribute("animate", "false"); - } - tempPanel.setAttribute("context", ""); - document.getElementById(CustomizableUI.AREA_NAVBAR).appendChild(tempPanel); - // If the view has a footer, set a convenience class on the panel. - tempPanel.classList.toggle("cui-widget-panelWithFooter", - viewNode.querySelector(".panel-subview-footer")); - - let multiView = document.createElement("panelmultiview"); - multiView.setAttribute("id", "customizationui-widget-multiview"); - multiView.setAttribute("nosubviews", "true"); - tempPanel.appendChild(multiView); - multiView.setAttribute("mainViewIsSubView", "true"); - multiView.setMainView(viewNode); - viewNode.classList.add("cui-widget-panelview"); - - let viewShown = false; - let panelRemover = () => { - viewNode.classList.remove("cui-widget-panelview"); - if (viewShown) { - CustomizableUI.removePanelCloseListeners(tempPanel); - tempPanel.removeEventListener("popuphidden", panelRemover); - - let evt = new CustomEvent("ViewHiding", {detail: viewNode}); - viewNode.dispatchEvent(evt); - } - aAnchor.open = false; - - this.multiView.appendChild(viewNode); - tempPanel.remove(); - }; - - // Emit the ViewShowing event so that the widget definition has a chance - // to lazily populate the subview with things. - let detail = { - blockers: new Set(), - addBlocker(aPromise) { - this.blockers.add(aPromise); - }, - }; - - let evt = new CustomEvent("ViewShowing", { bubbles: true, cancelable: true, detail }); - viewNode.dispatchEvent(evt); - - let cancel = evt.defaultPrevented; - if (detail.blockers.size) { - try { - let results = yield Promise.all(detail.blockers); - cancel = cancel || results.some(val => val === false); - } catch (e) { - Components.utils.reportError(e); - cancel = true; - } - } - - if (cancel) { - panelRemover(); - return; - } - - viewShown = true; - CustomizableUI.addPanelCloseListeners(tempPanel); - tempPanel.addEventListener("popuphidden", panelRemover); - - let iconAnchor = - document.getAnonymousElementByAttribute(aAnchor, "class", - "toolbarbutton-icon"); - - if (iconAnchor && aAnchor.id) { - iconAnchor.setAttribute("consumeanchor", aAnchor.id); - } - tempPanel.openPopup(iconAnchor || aAnchor, "bottomcenter topright"); - } - }), - - /** - * NB: The enable- and disableSingleSubviewPanelAnimations methods only - * affect the hiding/showing animations of single-subview panels (tempPanel - * in the showSubView method). - */ - disableSingleSubviewPanelAnimations: function() { - this._disableAnimations = true; - }, - - enableSingleSubviewPanelAnimations: function() { - this._disableAnimations = false; - }, - - onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer, aWasRemoval) { - if (aContainer != this.contents) { - return; - } - if (aWasRemoval) { - aNode.removeAttribute("auto-hyphens"); - } - }, - - onWidgetBeforeDOMChange: function(aNode, aNextNode, aContainer, aIsRemoval) { - if (aContainer != this.contents) { - return; - } - if (!aIsRemoval && - (this.panel.state == "open" || - document.documentElement.hasAttribute("customizing"))) { - this._adjustLabelsForAutoHyphens(aNode); - } - }, - - /** - * Signal that we're about to make a lot of changes to the contents of the - * panels all at once. For performance, we ignore the mutations. - */ - beginBatchUpdate: function() { - this._ensureEventListenersAdded(); - this.multiView.ignoreMutations = true; - }, - - /** - * Signal that we're done making bulk changes to the panel. We now pay - * attention to mutations. This automatically synchronizes the multiview - * container with whichever view is displayed if the panel is open. - */ - endBatchUpdate: function(aReason) { - this._ensureEventListenersAdded(); - this.multiView.ignoreMutations = false; - }, - - _adjustLabelsForAutoHyphens: function(aNode) { - let toolbarButtons = aNode ? [aNode] : - this.contents.querySelectorAll(".toolbarbutton-1"); - for (let node of toolbarButtons) { - let label = node.getAttribute("label"); - if (!label) { - continue; - } - if (label.includes("\u00ad")) { - node.setAttribute("auto-hyphens", "off"); - } else { - node.removeAttribute("auto-hyphens"); - } - } - }, - - /** - * Sets the anchor node into the open or closed state, depending - * on the state of the panel. - */ - _updatePanelButton: function() { - this.menuButton.open = this.panel.state == "open" || - this.panel.state == "showing"; - }, - - _onHelpViewShow: function(aEvent) { - // Call global menu setup function - buildHelpMenu(); - - let helpMenu = document.getElementById("menu_HelpPopup"); - let items = this.getElementsByTagName("vbox")[0]; - let attrs = ["oncommand", "onclick", "label", "key", "disabled"]; - let NSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; - - // Remove all buttons from the view - while (items.firstChild) { - items.removeChild(items.firstChild); - } - - // Add the current set of menuitems of the Help menu to this view - let menuItems = Array.prototype.slice.call(helpMenu.getElementsByTagName("menuitem")); - let fragment = document.createDocumentFragment(); - for (let node of menuItems) { - if (node.hidden) - continue; - let button = document.createElementNS(NSXUL, "toolbarbutton"); - // Copy specific attributes from a menuitem of the Help menu - for (let attrName of attrs) { - if (!node.hasAttribute(attrName)) - continue; - button.setAttribute(attrName, node.getAttribute(attrName)); - } - button.setAttribute("class", "subviewbutton"); - fragment.appendChild(button); - } - items.appendChild(fragment); - }, - - _updateQuitTooltip: function() { - if (AppConstants.platform == "win") { - return; - } - - let tooltipId = AppConstants.platform == "macosx" ? - "quit-button.tooltiptext.mac" : - "quit-button.tooltiptext.linux2"; - - let brands = Services.strings.createBundle("chrome://branding/locale/brand.properties"); - let stringArgs = [brands.GetStringFromName("brandShortName")]; - - let key = document.getElementById("key_quitApplication"); - stringArgs.push(ShortcutUtils.prettifyShortcut(key)); - let tooltipString = CustomizableUI.getLocalizedProperty({x: tooltipId}, "x", stringArgs); - let quitButton = document.getElementById("PanelUI-quit"); - quitButton.setAttribute("tooltiptext", tooltipString); - }, - - _overlayScrollListenerBoundFn: null, - _overlayScrollListener: function(aMQL) { - ScrollbarSampler.resetSystemScrollbarWidth(); - this._scrollWidth = null; - }, -}; - -XPCOMUtils.defineConstant(this, "PanelUI", PanelUI); - -/** - * Gets the currently selected locale for display. - * @return the selected locale or "en-US" if none is selected - */ -function getLocale() { - try { - let chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"] - .getService(Ci.nsIXULChromeRegistry); - return chromeRegistry.getSelectedLocale("browser"); - } catch (ex) { - return "en-US"; - } -} diff --git a/browser/components/customizableui/content/panelUI.xml b/browser/components/customizableui/content/panelUI.xml deleted file mode 100644 index 6893bd8ff..000000000 --- a/browser/components/customizableui/content/panelUI.xml +++ /dev/null @@ -1,509 +0,0 @@ -<?xml version="1.0"?> -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - -<bindings id="browserPanelUIBindings" - xmlns="http://www.mozilla.org/xbl" - xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" - xmlns:xbl="http://www.mozilla.org/xbl"> - - <binding id="panelmultiview"> - <resources> - <stylesheet src="chrome://browser/content/customizableui/panelUI.css"/> - </resources> - <content> - <xul:box anonid="viewContainer" class="panel-viewcontainer" xbl:inherits="panelopen,viewtype,transitioning"> - <xul:stack anonid="viewStack" xbl:inherits="viewtype,transitioning" viewtype="main" class="panel-viewstack"> - <xul:vbox anonid="mainViewContainer" class="panel-mainview" xbl:inherits="viewtype"/> - - <!-- Used to capture click events over the PanelUI-mainView if we're in - subview mode. That way, any click on the PanelUI-mainView causes us - to revert to the mainView mode, whereupon PanelUI-click-capture then - allows click events to go through it. --> - <xul:vbox anonid="clickCapturer" class="panel-clickcapturer"/> - - <!-- We manually set display: none (via a CSS attribute selector) on the - subviews that are not being displayed. We're using this over a deck - because a deck assumes the size of its largest child, regardless of - whether or not it is shown. That's not good for our case, since we - want to allow each subview to be uniquely sized. --> - <xul:vbox anonid="subViews" class="panel-subviews" xbl:inherits="panelopen"> - <children includes="panelview"/> - </xul:vbox> - </xul:stack> - </xul:box> - </content> - <implementation implements="nsIDOMEventListener"> - <field name="_clickCapturer" readonly="true"> - document.getAnonymousElementByAttribute(this, "anonid", "clickCapturer"); - </field> - <field name="_viewContainer" readonly="true"> - document.getAnonymousElementByAttribute(this, "anonid", "viewContainer"); - </field> - <field name="_mainViewContainer" readonly="true"> - document.getAnonymousElementByAttribute(this, "anonid", "mainViewContainer"); - </field> - <field name="_subViews" readonly="true"> - document.getAnonymousElementByAttribute(this, "anonid", "subViews"); - </field> - <field name="_viewStack" readonly="true"> - document.getAnonymousElementByAttribute(this, "anonid", "viewStack"); - </field> - <field name="_panel" readonly="true"> - this.parentNode; - </field> - - <field name="_currentSubView">null</field> - <field name="_anchorElement">null</field> - <field name="_mainViewHeight">0</field> - <field name="_subViewObserver">null</field> - <field name="__transitioning">false</field> - <field name="_ignoreMutations">false</field> - - <property name="showingSubView" readonly="true" - onget="return this._viewStack.getAttribute('viewtype') == 'subview'"/> - <property name="_mainViewId" onget="return this.getAttribute('mainViewId');" onset="this.setAttribute('mainViewId', val); return val;"/> - <property name="_mainView" readonly="true" - onget="return this._mainViewId ? document.getElementById(this._mainViewId) : null;"/> - <property name="showingSubViewAsMainView" readonly="true" - onget="return this.getAttribute('mainViewIsSubView') == 'true'"/> - - <property name="ignoreMutations"> - <getter> - return this._ignoreMutations; - </getter> - <setter><![CDATA[ - this._ignoreMutations = val; - if (!val && this._panel.state == "open") { - if (this.showingSubView) { - this._syncContainerWithSubView(); - } else { - this._syncContainerWithMainView(); - } - } - ]]></setter> - </property> - - <property name="_transitioning"> - <getter> - return this.__transitioning; - </getter> - <setter><![CDATA[ - this.__transitioning = val; - if (val) { - this.setAttribute("transitioning", "true"); - } else { - this.removeAttribute("transitioning"); - } - ]]></setter> - </property> - <constructor><![CDATA[ - this._clickCapturer.addEventListener("click", this); - this._panel.addEventListener("popupshowing", this); - this._panel.addEventListener("popupshown", this); - this._panel.addEventListener("popuphidden", this); - this._subViews.addEventListener("overflow", this); - this._mainViewContainer.addEventListener("overflow", this); - - // Get a MutationObserver ready to react to subview size changes. We - // only attach this MutationObserver when a subview is being displayed. - this._subViewObserver = - new MutationObserver(this._syncContainerWithSubView.bind(this)); - this._mainViewObserver = - new MutationObserver(this._syncContainerWithMainView.bind(this)); - - this._mainViewContainer.setAttribute("panelid", - this._panel.id); - - if (this._mainView) { - this.setMainView(this._mainView); - } - this.setAttribute("viewtype", "main"); - ]]></constructor> - - <destructor><![CDATA[ - if (this._mainView) { - this._mainView.removeAttribute("mainview"); - } - this._mainViewObserver.disconnect(); - this._subViewObserver.disconnect(); - this._panel.removeEventListener("popupshowing", this); - this._panel.removeEventListener("popupshown", this); - this._panel.removeEventListener("popuphidden", this); - this._subViews.removeEventListener("overflow", this); - this._mainViewContainer.removeEventListener("overflow", this); - this._clickCapturer.removeEventListener("click", this); - ]]></destructor> - - <method name="setMainView"> - <parameter name="aNewMainView"/> - <body><![CDATA[ - if (this._mainView) { - this._mainViewObserver.disconnect(); - this._subViews.appendChild(this._mainView); - this._mainView.removeAttribute("mainview"); - } - this._mainViewId = aNewMainView.id; - aNewMainView.setAttribute("mainview", "true"); - this._mainViewContainer.appendChild(aNewMainView); - ]]></body> - </method> - - <method name="showMainView"> - <body><![CDATA[ - if (this.showingSubView) { - let viewNode = this._currentSubView; - let evt = document.createEvent("CustomEvent"); - evt.initCustomEvent("ViewHiding", true, true, viewNode); - viewNode.dispatchEvent(evt); - - viewNode.removeAttribute("current"); - this._currentSubView = null; - - this._subViewObserver.disconnect(); - - this._setViewContainerHeight(this._mainViewHeight); - - this.setAttribute("viewtype", "main"); - } - - this._shiftMainView(); - ]]></body> - </method> - - <method name="showSubView"> - <parameter name="aViewId"/> - <parameter name="aAnchor"/> - <body><![CDATA[ - Task.spawn(function*() { - let viewNode = this.querySelector("#" + aViewId); - viewNode.setAttribute("current", true); - // Emit the ViewShowing event so that the widget definition has a chance - // to lazily populate the subview with things. - let detail = { - blockers: new Set(), - addBlocker(aPromise) { - this.blockers.add(aPromise); - }, - }; - - let evt = new CustomEvent("ViewShowing", { bubbles: true, cancelable: true, detail }); - viewNode.dispatchEvent(evt); - - let cancel = evt.defaultPrevented; - if (detail.blockers.size) { - try { - let results = yield Promise.all(detail.blockers); - cancel = cancel || results.some(val => val === false); - } catch (e) { - Components.utils.reportError(e); - cancel = true; - } - } - - if (cancel) { - return; - } - - this._currentSubView = viewNode; - - // Now we have to transition the panel. There are a few parts to this: - // - // 1) The main view content gets shifted so that the center of the anchor - // node is at the left-most edge of the panel. - // 2) The subview deck slides in so that it takes up almost all of the - // panel. - // 3) If the subview is taller then the main panel contents, then the panel - // must grow to meet that new height. Otherwise, it must shrink. - // - // All three of these actions make use of CSS transformations, so they - // should all occur simultaneously. - this.setAttribute("viewtype", "subview"); - this._shiftMainView(aAnchor); - - this._mainViewHeight = this._viewStack.clientHeight; - - let newHeight = this._heightOfSubview(viewNode, this._subViews); - this._setViewContainerHeight(newHeight); - - this._subViewObserver.observe(viewNode, { - attributes: true, - characterData: true, - childList: true, - subtree: true - }); - }.bind(this)); - ]]></body> - </method> - - <method name="_setViewContainerHeight"> - <parameter name="aHeight"/> - <body><![CDATA[ - let container = this._viewContainer; - this._transitioning = true; - - let onTransitionEnd = () => { - container.removeEventListener("transitionend", onTransitionEnd); - this._transitioning = false; - }; - - container.addEventListener("transitionend", onTransitionEnd); - container.style.height = `${aHeight}px`; - ]]></body> - </method> - - <method name="_shiftMainView"> - <parameter name="aAnchor"/> - <body><![CDATA[ - if (aAnchor) { - // We need to find the edge of the anchor, relative to the main panel. - // Then we need to add half the width of the anchor. This is the target - // that we need to transition to. - let anchorRect = aAnchor.getBoundingClientRect(); - let mainViewRect = this._mainViewContainer.getBoundingClientRect(); - let center = aAnchor.clientWidth / 2; - let direction = aAnchor.ownerDocument.defaultView.getComputedStyle(aAnchor, null).direction; - let edge; - if (direction == "ltr") { - edge = anchorRect.left - mainViewRect.left; - } else { - edge = mainViewRect.right - anchorRect.right; - } - - // If the anchor is an element on the far end of the mainView we - // don't want to shift the mainView too far, we would reveal empty - // space otherwise. - let cstyle = window.getComputedStyle(document.documentElement, null); - let exitSubViewGutterWidth = - cstyle.getPropertyValue("--panel-ui-exit-subview-gutter-width"); - let maxShift = mainViewRect.width - parseInt(exitSubViewGutterWidth); - let target = Math.min(maxShift, edge + center); - - let neg = direction == "ltr" ? "-" : ""; - this._mainViewContainer.style.transform = `translateX(${neg}${target}px)`; - aAnchor.setAttribute("panel-multiview-anchor", true); - } else { - this._mainViewContainer.style.transform = ""; - if (this.anchorElement) - this.anchorElement.removeAttribute("panel-multiview-anchor"); - } - this.anchorElement = aAnchor; - ]]></body> - </method> - - <method name="handleEvent"> - <parameter name="aEvent"/> - <body><![CDATA[ - if (aEvent.type.startsWith("popup") && aEvent.target != this._panel) { - // Shouldn't act on e.g. context menus being shown from within the panel. - return; - } - switch (aEvent.type) { - case "click": - if (aEvent.originalTarget == this._clickCapturer) { - this.showMainView(); - } - break; - case "overflow": - if (aEvent.target.localName == "vbox") { - // Resize the right view on the next tick. - if (this.showingSubView) { - setTimeout(this._syncContainerWithSubView.bind(this), 0); - } else if (!this.transitioning) { - setTimeout(this._syncContainerWithMainView.bind(this), 0); - } - } - break; - case "popupshowing": - this.setAttribute("panelopen", "true"); - // Bug 941196 - The panel can get taller when opening a subview. Disabling - // autoPositioning means that the panel won't jump around if an opened - // subview causes the panel to exceed the dimensions of the screen in the - // direction that the panel originally opened in. This property resets - // every time the popup closes, which is why we have to set it each time. - this._panel.autoPosition = false; - this._syncContainerWithMainView(); - - this._mainViewObserver.observe(this._mainView, { - attributes: true, - characterData: true, - childList: true, - subtree: true - }); - - break; - case "popupshown": - this._setMaxHeight(); - break; - case "popuphidden": - this.removeAttribute("panelopen"); - this._mainView.style.removeProperty("height"); - this.showMainView(); - this._mainViewObserver.disconnect(); - break; - } - ]]></body> - </method> - - <method name="_shouldSetPosition"> - <body><![CDATA[ - return this.getAttribute("nosubviews") == "true"; - ]]></body> - </method> - - <method name="_shouldSetHeight"> - <body><![CDATA[ - return this.getAttribute("nosubviews") != "true"; - ]]></body> - </method> - - <method name="_setMaxHeight"> - <body><![CDATA[ - if (!this._shouldSetHeight()) - return; - - // Ignore the mutation that'll fire when we set the height of - // the main view. - this.ignoreMutations = true; - this._mainView.style.height = - this.getBoundingClientRect().height + "px"; - this.ignoreMutations = false; - ]]></body> - </method> - <method name="_adjustContainerHeight"> - <body><![CDATA[ - if (!this.ignoreMutations && !this.showingSubView && !this._transitioning) { - let height; - if (this.showingSubViewAsMainView) { - height = this._heightOfSubview(this._mainView); - } else { - height = this._mainView.scrollHeight; - } - this._viewContainer.style.height = height + "px"; - } - ]]></body> - </method> - <method name="_syncContainerWithSubView"> - <body><![CDATA[ - // Check that this panel is still alive: - if (!this._panel || !this._panel.parentNode) { - return; - } - - if (!this.ignoreMutations && this.showingSubView) { - let newHeight = this._heightOfSubview(this._currentSubView, this._subViews); - this._viewContainer.style.height = newHeight + "px"; - } - ]]></body> - </method> - <method name="_syncContainerWithMainView"> - <body><![CDATA[ - // Check that this panel is still alive: - if (!this._panel || !this._panel.parentNode) { - return; - } - - if (this._shouldSetPosition()) { - this._panel.adjustArrowPosition(); - } - - if (this._shouldSetHeight()) { - this._adjustContainerHeight(); - } - ]]></body> - </method> - - <!-- Call this when the height of one of your views (the main view or a - subview) changes and you want the heights of the multiview and panel - to be the same as the view's height. - If the caller can give a hint of the expected height change with the - optional aExpectedChange parameter, it prevents flicker. --> - <method name="setHeightToFit"> - <parameter name="aExpectedChange"/> - <body><![CDATA[ - // Set the max-height to zero, wait until the height is actually - // updated, and then remove it. If it's not removed, weird things can - // happen, like widgets in the panel won't respond to clicks even - // though they're visible. - let count = 5; - let height = getComputedStyle(this).height; - if (aExpectedChange) - this.style.maxHeight = (parseInt(height) + aExpectedChange) + "px"; - else - this.style.maxHeight = "0"; - let interval = setInterval(() => { - if (height != getComputedStyle(this).height || --count == 0) { - clearInterval(interval); - this.style.removeProperty("max-height"); - } - }, 0); - ]]></body> - </method> - - <method name="_heightOfSubview"> - <parameter name="aSubview"/> - <parameter name="aContainerToCheck"/> - <body><![CDATA[ - function getFullHeight(element) { - // XXXgijs: unfortunately, scrollHeight rounds values, and there's no alternative - // that works with overflow: auto elements. Fortunately for us, - // we have exactly 1 (potentially) scrolling element in here (the subview body), - // and rounding 1 value is OK - rounding more than 1 and adding them means we get - // off-by-1 errors. Now we might be off by a subpixel, but we care less about that. - // So, use scrollHeight *only* if the element is vertically scrollable. - let height; - let elementCS; - if (element.scrollTopMax) { - height = element.scrollHeight; - // Bounding client rects include borders, scrollHeight doesn't: - elementCS = win.getComputedStyle(element); - height += parseFloat(elementCS.borderTopWidth) + - parseFloat(elementCS.borderBottomWidth); - } else { - height = element.getBoundingClientRect().height; - if (height > 0) { - elementCS = win.getComputedStyle(element); - } - } - if (elementCS) { - // Include margins - but not borders or paddings because they - // were dealt with above. - height += parseFloat(elementCS.marginTop) + parseFloat(elementCS.marginBottom); - } - return height; - } - let win = aSubview.ownerDocument.defaultView; - let body = aSubview.querySelector(".panel-subview-body"); - let height = getFullHeight(body || aSubview); - if (body) { - let header = aSubview.querySelector(".panel-subview-header"); - let footer = aSubview.querySelector(".panel-subview-footer"); - height += (header ? getFullHeight(header) : 0) + - (footer ? getFullHeight(footer) : 0); - } - if (aContainerToCheck) { - let containerCS = win.getComputedStyle(aContainerToCheck); - height += parseFloat(containerCS.paddingTop) + parseFloat(containerCS.paddingBottom); - } - return Math.ceil(height); - ]]></body> - </method> - - </implementation> - </binding> - - <binding id="panelview"> - <implementation> - <property name="panelMultiView" readonly="true"> - <getter><![CDATA[ - if (this.parentNode.localName != "panelmultiview") { - return document.getBindingParent(this.parentNode); - } - - return this.parentNode; - ]]></getter> - </property> - </implementation> - </binding> -</bindings> diff --git a/browser/components/customizableui/content/toolbar.xml b/browser/components/customizableui/content/toolbar.xml deleted file mode 100644 index 4e6964c9f..000000000 --- a/browser/components/customizableui/content/toolbar.xml +++ /dev/null @@ -1,618 +0,0 @@ -<?xml version="1.0"?> -<!-- This Source Code Form is subject to the terms of the Mozilla Public - - License, v. 2.0. If a copy of the MPL was not distributed with this - - file, You can obtain one at http://mozilla.org/MPL/2.0/. --> - -<bindings id="browserToolbarBindings" - xmlns="http://www.mozilla.org/xbl" - xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul" - xmlns:xbl="http://www.mozilla.org/xbl"> - - <binding id="toolbar" role="xul:toolbar"> - <resources> - <stylesheet src="chrome://global/skin/toolbar.css"/> - </resources> - <implementation> - <field name="overflowedDuringConstruction">null</field> - - <constructor><![CDATA[ - let scope = {}; - Cu.import("resource:///modules/CustomizableUI.jsm", scope); - // Add an early overflow event listener that will mark if the - // toolbar overflowed during construction. - if (scope.CustomizableUI.isAreaOverflowable(this.id)) { - this.addEventListener("overflow", this); - this.addEventListener("underflow", this); - } - - if (document.readyState == "complete") { - this._init(); - } else { - // Need to wait until XUL overlays are loaded. See bug 554279. - let self = this; - document.addEventListener("readystatechange", function onReadyStateChange() { - if (document.readyState != "complete") - return; - document.removeEventListener("readystatechange", onReadyStateChange, false); - self._init(); - }, false); - } - ]]></constructor> - - <method name="_init"> - <body><![CDATA[ - let scope = {}; - Cu.import("resource:///modules/CustomizableUI.jsm", scope); - let CustomizableUI = scope.CustomizableUI; - - // Bug 989289: Forcibly set the now unsupported "mode" and "iconsize" - // attributes, just in case they accidentally get restored from - // persistence from a user that's been upgrading and downgrading. - if (CustomizableUI.isBuiltinToolbar(this.id)) { - const kAttributes = new Map([["mode", "icons"], ["iconsize", "small"]]); - for (let [attribute, value] of kAttributes) { - if (this.getAttribute(attribute) != value) { - this.setAttribute(attribute, value); - document.persist(this.id, attribute); - } - if (this.toolbox) { - if (this.toolbox.getAttribute(attribute) != value) { - this.toolbox.setAttribute(attribute, value); - document.persist(this.toolbox.id, attribute); - } - } - } - } - - // Searching for the toolbox palette in the toolbar binding because - // toolbars are constructed first. - let toolbox = this.toolbox; - if (toolbox && !toolbox.palette) { - for (let node of toolbox.children) { - if (node.localName == "toolbarpalette") { - // Hold on to the palette but remove it from the document. - toolbox.palette = node; - toolbox.removeChild(node); - break; - } - } - } - - // pass the current set of children for comparison with placements: - let children = Array.from(this.childNodes) - .filter(node => node.getAttribute("skipintoolbarset") != "true" && node.id) - .map(node => node.id); - CustomizableUI.registerToolbarNode(this, children); - ]]></body> - </method> - - <method name="handleEvent"> - <parameter name="aEvent"/> - <body><![CDATA[ - if (aEvent.type == "overflow" && aEvent.detail > 0) { - if (this.overflowable && this.overflowable.initialized) { - this.overflowable.onOverflow(aEvent); - } else { - this.overflowedDuringConstruction = aEvent; - } - } else if (aEvent.type == "underflow" && aEvent.detail > 0) { - this.overflowedDuringConstruction = null; - } - ]]></body> - </method> - - <method name="insertItem"> - <parameter name="aId"/> - <parameter name="aBeforeElt"/> - <parameter name="aWrapper"/> - <body><![CDATA[ - if (aWrapper) { - Cu.reportError("Can't insert " + aId + ": using insertItem " + - "no longer supports wrapper elements."); - return null; - } - - // Hack, the customizable UI code makes this be the last position - let pos = null; - if (aBeforeElt) { - let beforeInfo = CustomizableUI.getPlacementOfWidget(aBeforeElt.id); - if (beforeInfo.area != this.id) { - Cu.reportError("Can't insert " + aId + " before " + - aBeforeElt.id + " which isn't in this area (" + - this.id + ")."); - return null; - } - pos = beforeInfo.position; - } - - CustomizableUI.addWidgetToArea(aId, this.id, pos); - return this.ownerDocument.getElementById(aId); - ]]></body> - </method> - - <property name="toolbarName" - onget="return this.getAttribute('toolbarname');" - onset="this.setAttribute('toolbarname', val); return val;"/> - - <property name="customizationTarget" readonly="true"> - <getter><![CDATA[ - if (this._customizationTarget) - return this._customizationTarget; - - let id = this.getAttribute("customizationtarget"); - if (id) - this._customizationTarget = document.getElementById(id); - - if (this._customizationTarget) - this._customizationTarget.insertItem = this.insertItem.bind(this); - else - this._customizationTarget = this; - - return this._customizationTarget; - ]]></getter> - </property> - - <property name="toolbox" readonly="true"> - <getter><![CDATA[ - if (this._toolbox) - return this._toolbox; - - let toolboxId = this.getAttribute("toolboxid"); - if (toolboxId) { - let toolbox = document.getElementById(toolboxId); - if (toolbox) { - if (toolbox.externalToolbars.indexOf(this) == -1) - toolbox.externalToolbars.push(this); - - this._toolbox = toolbox; - } - } - - if (!this._toolbox && this.parentNode && - this.parentNode.localName == "toolbox") { - this._toolbox = this.parentNode; - } - - return this._toolbox; - ]]></getter> - </property> - - <property name="currentSet"> - <getter><![CDATA[ - let currentWidgets = new Set(); - for (let node of this.customizationTarget.children) { - let realNode = node.localName == "toolbarpaletteitem" ? node.firstChild : node; - if (realNode.getAttribute("skipintoolbarset") != "true") { - currentWidgets.add(realNode.id); - } - } - if (this.getAttribute("overflowing") == "true") { - let overflowTarget = this.getAttribute("overflowtarget"); - let overflowList = this.ownerDocument.getElementById(overflowTarget); - for (let node of overflowList.children) { - let realNode = node.localName == "toolbarpaletteitem" ? node.firstChild : node; - if (realNode.getAttribute("skipintoolbarset") != "true") { - currentWidgets.add(realNode.id); - } - } - } - let orderedPlacements = CustomizableUI.getWidgetIdsInArea(this.id); - return orderedPlacements.filter((x) => currentWidgets.has(x)).join(','); - ]]></getter> - <setter><![CDATA[ - // Get list of new and old ids: - let newVal = (val || '').split(',').filter(x => x); - let oldIds = CustomizableUI.getWidgetIdsInArea(this.id); - - // Get a list of items only in the new list - let newIds = newVal.filter(id => oldIds.indexOf(id) == -1); - CustomizableUI.beginBatchUpdate(); - try { - for (let newId of newIds) { - oldIds = CustomizableUI.getWidgetIdsInArea(this.id); - let nextId = newId; - let pos; - do { - // Get the next item - nextId = newVal[newVal.indexOf(nextId) + 1]; - // Figure out where it is in the old list - pos = oldIds.indexOf(nextId); - // If it's not in the old list, repeat: - } while (pos == -1 && nextId); - if (pos == -1) { - pos = null; // We didn't find anything, insert at the end - } - CustomizableUI.addWidgetToArea(newId, this.id, pos); - } - - let currentIds = this.currentSet.split(','); - let removedIds = currentIds.filter(id => newIds.indexOf(id) == -1 && newVal.indexOf(id) == -1); - for (let removedId of removedIds) { - CustomizableUI.removeWidgetFromArea(removedId); - } - } finally { - CustomizableUI.endBatchUpdate(); - } - ]]></setter> - </property> - - - </implementation> - </binding> - - <binding id="toolbar-menubar-stub"> - <implementation> - <property name="toolbox" readonly="true"> - <getter><![CDATA[ - if (this._toolbox) - return this._toolbox; - - if (this.parentNode && this.parentNode.localName == "toolbox") { - this._toolbox = this.parentNode; - } - - return this._toolbox; - ]]></getter> - </property> - <property name="currentSet" readonly="true"> - <getter><![CDATA[ - return this.getAttribute("defaultset"); - ]]></getter> - </property> - <method name="insertItem"> - <body><![CDATA[ - return null; - ]]></body> - </method> - </implementation> - </binding> - - <!-- The toolbar-menubar-autohide and toolbar-drag bindings are almost - verbatim copies of their toolkit counterparts - they just inherit from - the customizableui's toolbar binding instead of toolkit's. We're currently - OK with the maintainance burden of having two copies of a binding, since - the long term goal is to move the customization framework into toolkit. --> - - <binding id="toolbar-menubar-autohide" - extends="chrome://browser/content/customizableui/toolbar.xml#toolbar"> - <implementation> - <constructor> - this._setInactive(); - </constructor> - <destructor> - this._setActive(); - </destructor> - - <field name="_inactiveTimeout">null</field> - - <field name="_contextMenuListener"><![CDATA[({ - toolbar: this, - contextMenu: null, - - get active () { - return !!this.contextMenu; - }, - - init: function (event) { - let node = event.target; - while (node != this.toolbar) { - if (node.localName == "menupopup") - return; - node = node.parentNode; - } - - let contextMenuId = this.toolbar.getAttribute("context"); - if (!contextMenuId) - return; - - this.contextMenu = document.getElementById(contextMenuId); - if (!this.contextMenu) - return; - - this.contextMenu.addEventListener("popupshown", this, false); - this.contextMenu.addEventListener("popuphiding", this, false); - this.toolbar.addEventListener("mousemove", this, false); - }, - handleEvent: function (event) { - switch (event.type) { - case "popupshown": - this.toolbar.removeEventListener("mousemove", this, false); - break; - case "popuphiding": - case "mousemove": - this.toolbar._setInactiveAsync(); - this.toolbar.removeEventListener("mousemove", this, false); - this.contextMenu.removeEventListener("popuphiding", this, false); - this.contextMenu.removeEventListener("popupshown", this, false); - this.contextMenu = null; - break; - } - } - })]]></field> - - <method name="_setInactive"> - <body><![CDATA[ - this.setAttribute("inactive", "true"); - ]]></body> - </method> - - <method name="_setInactiveAsync"> - <body><![CDATA[ - this._inactiveTimeout = setTimeout(function (self) { - if (self.getAttribute("autohide") == "true") { - self._inactiveTimeout = null; - self._setInactive(); - } - }, 0, this); - ]]></body> - </method> - - <method name="_setActive"> - <body><![CDATA[ - if (this._inactiveTimeout) { - clearTimeout(this._inactiveTimeout); - this._inactiveTimeout = null; - } - this.removeAttribute("inactive"); - ]]></body> - </method> - </implementation> - - <handlers> - <handler event="DOMMenuBarActive" action="this._setActive();"/> - <handler event="popupshowing" action="this._setActive();"/> - <handler event="mousedown" button="2" action="this._contextMenuListener.init(event);"/> - <handler event="DOMMenuBarInactive"><![CDATA[ - if (!this._contextMenuListener.active) - this._setInactiveAsync(); - ]]></handler> - </handlers> - </binding> - - <binding id="toolbar-drag" - extends="chrome://browser/content/customizableui/toolbar.xml#toolbar"> - <implementation> - <field name="_dragBindingAlive">true</field> - <constructor><![CDATA[ - if (!this._draggableStarted) { - this._draggableStarted = true; - try { - let tmp = {}; - Components.utils.import("resource://gre/modules/WindowDraggingUtils.jsm", tmp); - let draggableThis = new tmp.WindowDraggingElement(this); - draggableThis.mouseDownCheck = function(e) { - return this._dragBindingAlive; - }; - } catch (e) {} - } - ]]></constructor> - </implementation> - </binding> - - -<!-- This is a peculiar binding. It is here to deal with overlayed/inserted add-on content, - and immediately direct such content elsewhere. --> - <binding id="addonbar-delegating"> - <implementation> - <constructor><![CDATA[ - // Reading these immediately so nobody messes with them anymore: - this._delegatingToolbar = this.getAttribute("toolbar-delegate"); - this._wasCollapsed = this.getAttribute("collapsed") == "true"; - // Leaving those in here to unbreak some code: - if (document.readyState == "complete") { - this._init(); - } else { - // Need to wait until XUL overlays are loaded. See bug 554279. - let self = this; - document.addEventListener("readystatechange", function onReadyStateChange() { - if (document.readyState != "complete") - return; - document.removeEventListener("readystatechange", onReadyStateChange, false); - self._init(); - }, false); - } - ]]></constructor> - - <method name="_init"> - <body><![CDATA[ - // Searching for the toolbox palette in the toolbar binding because - // toolbars are constructed first. - let toolbox = this.toolbox; - if (toolbox && !toolbox.palette) { - for (let node of toolbox.children) { - if (node.localName == "toolbarpalette") { - // Hold on to the palette but remove it from the document. - toolbox.palette = node; - toolbox.removeChild(node); - } - } - } - - // pass the current set of children for comparison with placements: - let children = []; - for (let node of this.childNodes) { - if (node.getAttribute("skipintoolbarset") != "true" && node.id) { - // Force everything to be removable so that buildArea can chuck stuff - // out if the user has customized things / we've been here before: - if (!this._whiteListed.has(node.id)) { - node.setAttribute("removable", "true"); - } - children.push(node); - } - } - CustomizableUI.registerToolbarNode(this, children); - let existingMigratedItems = (this.getAttribute("migratedset") || "").split(','); - for (let migratedItem of existingMigratedItems.filter((x) => !!x)) { - this._currentSetMigrated.add(migratedItem); - } - this.evictNodes(); - // We can't easily use |this| or strong bindings for the observer fn here - // because that creates leaky circular references when the node goes away, - // and XBL destructors are unreliable. - let mutationObserver = new MutationObserver(function(mutations) { - if (!mutations.length) { - return; - } - let toolbar = mutations[0].target; - // Can't use our own attribute because we might not have one if we're set to - // collapsed - let areCustomizing = toolbar.ownerDocument.documentElement.getAttribute("customizing"); - if (!toolbar._isModifying && !areCustomizing) { - toolbar.evictNodes(); - } - }); - mutationObserver.observe(this, {childList: true}); - ]]></body> - </method> - <method name="evictNodes"> - <body><![CDATA[ - this._isModifying = true; - let i = this.childNodes.length; - while (i--) { - let node = this.childNodes[i]; - if (this.childNodes[i].id) { - this.evictNode(this.childNodes[i]); - } else { - node.remove(); - } - } - this._isModifying = false; - this._updateMigratedSet(); - ]]></body> - </method> - <method name="evictNode"> - <parameter name="aNode"/> - <body> - <![CDATA[ - if (this._whiteListed.has(aNode.id) || CustomizableUI.isSpecialWidget(aNode.id)) { - return; - } - const kItemMaxWidth = 100; - let oldParent = aNode.parentNode; - aNode.setAttribute("removable", "true"); - this._currentSetMigrated.add(aNode.id); - - let movedOut = false; - if (!this._wasCollapsed) { - try { - let nodeWidth = aNode.getBoundingClientRect().width; - if (nodeWidth == 0 || nodeWidth > kItemMaxWidth) { - throw new Error(aNode.id + " is too big (" + nodeWidth + - "px wide), moving to the palette"); - } - CustomizableUI.addWidgetToArea(aNode.id, this._delegatingToolbar); - movedOut = true; - } catch (ex) { - // This will throw if the node is too big, or can't be moved there for - // some reason. Report this: - Cu.reportError(ex); - } - } - - /* We won't have moved the widget if either the add-on bar was collapsed, - * or if it was too wide to be inserted into the navbar. */ - if (!movedOut) { - try { - CustomizableUI.removeWidgetFromArea(aNode.id); - } catch (ex) { - Cu.reportError(ex); - aNode.remove(); - } - } - - // Surprise: addWidgetToArea(palette) will get you nothing if the palette - // is not constructed yet. Fix: - if (aNode.parentNode == oldParent) { - let palette = this.toolbox.palette; - if (palette && oldParent != palette) { - palette.appendChild(aNode); - } - } - ]]></body> - </method> - <method name="insertItem"> - <parameter name="aId"/> - <parameter name="aBeforeElt"/> - <parameter name="aWrapper"/> - <body><![CDATA[ - if (aWrapper) { - Cu.reportError("Can't insert " + aId + ": using insertItem " + - "no longer supports wrapper elements."); - return null; - } - - let widget = CustomizableUI.getWidget(aId); - widget = widget && widget.forWindow(window); - let node = widget && widget.node; - if (!node) { - return null; - } - - this._isModifying = true; - // Temporarily add it here so it can have a width, then ditch it: - this.appendChild(node); - this.evictNode(node); - this._isModifying = false; - this._updateMigratedSet(); - // We will now have moved stuff around; kick off some events - // so add-ons know we've just moved their stuff: - // XXXgijs: only in this window. It's hard to know for sure what's the right - // thing to do here - typically insertItem is used on each window, so - // this seems to make the most sense, even if some of the effects of - // evictNode might affect multiple windows. - CustomizableUI.dispatchToolboxEvent("customizationchange", {}, window); - CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, window); - return node; - ]]></body> - </method> - <method name="getMigratedItems"> - <body><![CDATA[ - return [... this._currentSetMigrated]; - ]]></body> - </method> - <method name="_updateMigratedSet"> - <body><![CDATA[ - let newMigratedItems = this.getMigratedItems().join(','); - if (this.getAttribute("migratedset") != newMigratedItems) { - this.setAttribute("migratedset", newMigratedItems); - this.ownerDocument.persist(this.id, "migratedset"); - } - ]]></body> - </method> - <property name="customizationTarget" readonly="true"> - <getter><![CDATA[ - return this; - ]]></getter> - </property> - <property name="currentSet"> - <getter><![CDATA[ - return Array.from(this.children, node => node.id).join(","); - ]]></getter> - <setter><![CDATA[ - let v = val.split(','); - let newButtons = v.filter(x => x && (!this._whiteListed.has(x) && - !CustomizableUI.isSpecialWidget(x) && - !this._currentSetMigrated.has(x))); - for (let newButton of newButtons) { - this._currentSetMigrated.add(newButton); - this.insertItem(newButton); - } - this._updateMigratedSet(); - ]]></setter> - </property> - <property name="toolbox" readonly="true"> - <getter><![CDATA[ - if (!this._toolbox && this.parentNode && - this.parentNode.localName == "toolbox") { - this._toolbox = this.parentNode; - } - - return this._toolbox; - ]]></getter> - </property> - <field name="_whiteListed" readonly="true">new Set(["addonbar-closebutton", "status-bar"])</field> - <field name="_isModifying">false</field> - <field name="_currentSetMigrated">new Set()</field> - </implementation> - </binding> -</bindings> diff --git a/browser/components/customizableui/moz.build b/browser/components/customizableui/moz.build deleted file mode 100644 index 034630dc9..000000000 --- a/browser/components/customizableui/moz.build +++ /dev/null @@ -1,21 +0,0 @@ -# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*- -# vim: set filetype=python: -# This Source Code Form is subject to the terms of the Mozilla Public -# License, v. 2.0. If a copy of the MPL was not distributed with this -# file, You can obtain one at http://mozilla.org/MPL/2.0/. - -DIRS += [ - 'content', -] - -EXTRA_JS_MODULES += [ - 'CustomizableUI.jsm', - 'CustomizableWidgets.jsm', - 'CustomizeMode.jsm', - 'DragPositionManager.jsm', - 'PanelWideWidgetTracker.jsm', - 'ScrollbarSampler.jsm', -] - -if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'cocoa'): - DEFINES['CAN_DRAW_IN_TITLEBAR'] = 1 |