diff options
Diffstat (limited to 'browser/components/customizableui')
129 files changed, 20009 insertions, 0 deletions
diff --git a/browser/components/customizableui/CustomizableUI.jsm b/browser/components/customizableui/CustomizableUI.jsm new file mode 100644 index 000000000..86ff2708b --- /dev/null +++ b/browser/components/customizableui/CustomizableUI.jsm @@ -0,0 +1,4420 @@ +/* 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"); + } + + if (AppConstants.E10S_TESTING_ONLY) { + if (gPalette.has("e10s-button")) { + let newWindowIndex = panelPlacements.indexOf("new-window-button"); + if (newWindowIndex > -1) { + panelPlacements.splice(newWindowIndex + 1, 0, "e10s-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.platform != "macosx") { + this.registerArea(CustomizableUI.AREA_MENUBAR, { + legacy: true, + type: CustomizableUI.TYPE_TOOLBAR, + defaultPlacements: [ + "menubar-items", + ], + get defaultCollapsed() { + if (AppConstants.MENUBAR_CAN_AUTOHIDE) { + if (AppConstants.platform == "linux") { + return true; + } + // This is duplicated logic from /browser/base/jar.mn + // for win6BrowserOverlay.xul. + return AppConstants.isPlatformAndVersionAtLeast("win", 6); + } + return false; + } + }, true); + } + + this.registerArea(CustomizableUI.AREA_TABSTRIP, { + legacy: true, + type: CustomizableUI.TYPE_TOOLBAR, + defaultPlacements: [ + "tabbrowser-tabs", + "new-tab-button", + "alltabs-button", + ], + defaultCollapsed: null, + }, true); + this.registerArea(CustomizableUI.AREA_BOOKMARKS, { + legacy: true, + type: CustomizableUI.TYPE_TOOLBAR, + defaultPlacements: [ + "personal-bookmarks", + ], + defaultCollapsed: true, + }, true); + + this.registerArea(CustomizableUI.AREA_ADDONBAR, { + type: CustomizableUI.TYPE_TOOLBAR, + legacy: true, + defaultPlacements: ["addonbar-closebutton", "status-bar"], + defaultCollapsed: false, + }, true); + }, + + get _builtinToolbars() { + let toolbars = new Set([ + CustomizableUI.AREA_NAVBAR, + CustomizableUI.AREA_BOOKMARKS, + CustomizableUI.AREA_TABSTRIP, + CustomizableUI.AREA_ADDONBAR, + ]); + if (AppConstants.platform != "macosx") { + toolbars.add(CustomizableUI.AREA_MENUBAR); + } + return toolbars; + }, + + _defineBuiltInWidgets: function() { + for (let widgetDefinition of CustomizableWidgets) { + this.createBuiltinWidget(widgetDefinition); + } + }, + + _introduceNewBuiltinWidgets: function() { + // We should still enter even if gSavedState.currentVersion >= kVersion + // because the per-widget pref facility is independent of versioning. + if (!gSavedState) { + // Flip all the prefs so we don't try to re-introduce later: + for (let [, widget] of gPalette) { + if (widget.defaultArea && widget._introducedInVersion === "pref") { + let prefId = "browser.toolbarbuttons.introduced." + widget.id; + Services.prefs.setBoolPref(prefId, true); + } + } + return; + } + + let currentVersion = gSavedState.currentVersion; + for (let [id, widget] of gPalette) { + if (widget.defaultArea) { + let shouldAdd = false; + let shouldSetPref = false; + let prefId = "browser.toolbarbuttons.introduced." + widget.id; + if (widget._introducedInVersion === "pref") { + try { + shouldAdd = !Services.prefs.getBoolPref(prefId); + } catch (ex) { + // Pref doesn't exist: + shouldAdd = true; + } + shouldSetPref = shouldAdd; + } else if (widget._introducedInVersion > currentVersion) { + shouldAdd = true; + } + + if (shouldAdd) { + let futurePlacements = gFuturePlacements.get(widget.defaultArea); + if (futurePlacements) { + futurePlacements.add(id); + } else { + gFuturePlacements.set(widget.defaultArea, new Set([id])); + } + if (shouldSetPref) { + Services.prefs.setBoolPref(prefId, true); + } + } + } + } + + if (currentVersion < 2) { + // Nuke the old 'loop-call-button' out of orbit. + CustomizableUI.removeWidgetFromArea("loop-call-button"); + } + + if (currentVersion < 4) { + CustomizableUI.removeWidgetFromArea("loop-button-throttled"); + } + }, + + /** + * _markObsoleteBuiltinButtonsSeen + * when upgrading, ensure obsoleted buttons are in seen state. + */ + _markObsoleteBuiltinButtonsSeen: function() { + if (!gSavedState) + return; + let currentVersion = gSavedState.currentVersion; + if (currentVersion >= kVersion) + return; + // we're upgrading, update state if necessary + for (let id in ObsoleteBuiltinButtons) { + let version = ObsoleteBuiltinButtons[id] + if (version == kVersion) { + gSeenWidgets.add(id); + gDirty = true; + } + } + }, + + _placeNewDefaultWidgetsInArea: function(aArea) { + let futurePlacedWidgets = gFuturePlacements.get(aArea); + let savedPlacements = gSavedState && gSavedState.placements && gSavedState.placements[aArea]; + let defaultPlacements = gAreas.get(aArea).get("defaultPlacements"); + if (!savedPlacements || !savedPlacements.length || !futurePlacedWidgets || !defaultPlacements || + !defaultPlacements.length) { + return; + } + let defaultWidgetIndex = -1; + + for (let widgetId of futurePlacedWidgets) { + let widget = gPalette.get(widgetId); + if (!widget || widget.source !== CustomizableUI.SOURCE_BUILTIN || + !widget.defaultArea || !widget._introducedInVersion || + savedPlacements.indexOf(widget.id) !== -1) { + continue; + } + defaultWidgetIndex = defaultPlacements.indexOf(widget.id); + if (defaultWidgetIndex === -1) { + continue; + } + // Now we know that this widget should be here by default, was newly introduced, + // and we have a saved state to insert into, and a default state to work off of. + // Try introducing after widgets that come before it in the default placements: + for (let i = defaultWidgetIndex; i >= 0; i--) { + // Special case: if the defaults list this widget as coming first, insert at the beginning: + if (i === 0 && i === defaultWidgetIndex) { + savedPlacements.splice(0, 0, widget.id); + // Before you ask, yes, deleting things inside a let x of y loop where y is a Set is + // safe, and we won't skip any items. + futurePlacedWidgets.delete(widget.id); + gDirty = true; + break; + } + // Otherwise, if we're somewhere other than the beginning, check if the previous + // widget is in the saved placements. + if (i) { + let previousWidget = defaultPlacements[i - 1]; + let previousWidgetIndex = savedPlacements.indexOf(previousWidget); + if (previousWidgetIndex != -1) { + savedPlacements.splice(previousWidgetIndex + 1, 0, widget.id); + futurePlacedWidgets.delete(widget.id); + gDirty = true; + break; + } + } + } + // The loop above either inserts the item or doesn't - either way, we can get away + // with doing nothing else now; if the item remains in gFuturePlacements, we'll + // add it at the end in restoreStateForArea. + } + this.saveState(); + }, + + wrapWidget: function(aWidgetId) { + if (gGroupWrapperCache.has(aWidgetId)) { + return gGroupWrapperCache.get(aWidgetId); + } + + let provider = this.getWidgetProvider(aWidgetId); + if (!provider) { + return null; + } + + if (provider == CustomizableUI.PROVIDER_API) { + let widget = gPalette.get(aWidgetId); + if (!widget.wrapper) { + widget.wrapper = new WidgetGroupWrapper(widget); + gGroupWrapperCache.set(aWidgetId, widget.wrapper); + } + return widget.wrapper; + } + + // PROVIDER_SPECIAL gets treated the same as PROVIDER_XUL. + let wrapper = new XULWidgetGroupWrapper(aWidgetId); + gGroupWrapperCache.set(aWidgetId, wrapper); + return wrapper; + }, + + registerArea: function(aName, aProperties, aInternalCaller) { + if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) { + throw new Error("Invalid area name"); + } + + let areaIsKnown = gAreas.has(aName); + let props = areaIsKnown ? gAreas.get(aName) : new Map(); + const kImmutableProperties = new Set(["type", "legacy", "overflowable"]); + for (let key in aProperties) { + if (areaIsKnown && kImmutableProperties.has(key) && + props.get(key) != aProperties[key]) { + throw new Error("An area cannot change the property for '" + key + "'"); + } + // XXXgijs for special items, we need to make sure they have an appropriate ID + // so we aren't perpetually in a non-default state: + if (key == "defaultPlacements" && Array.isArray(aProperties[key])) { + props.set(key, aProperties[key].map(x => this.isSpecialWidget(x) ? this.ensureSpecialWidgetId(x) : x )); + } else { + props.set(key, aProperties[key]); + } + } + // Default to a toolbar: + if (!props.has("type")) { + props.set("type", CustomizableUI.TYPE_TOOLBAR); + } + if (props.get("type") == CustomizableUI.TYPE_TOOLBAR) { + // Check aProperties instead of props because this check is only interested + // in the passed arguments, not the state of a potentially pre-existing area. + if (!aInternalCaller && aProperties["defaultCollapsed"]) { + throw new Error("defaultCollapsed is only allowed for default toolbars.") + } + if (!props.has("defaultCollapsed")) { + props.set("defaultCollapsed", true); + } + } else if (props.has("defaultCollapsed")) { + throw new Error("defaultCollapsed only applies for TYPE_TOOLBAR areas."); + } + // Sanity check type: + let allTypes = [CustomizableUI.TYPE_TOOLBAR, CustomizableUI.TYPE_MENU_PANEL]; + if (allTypes.indexOf(props.get("type")) == -1) { + throw new Error("Invalid area type " + props.get("type")); + } + + // And to no placements: + if (!props.has("defaultPlacements")) { + props.set("defaultPlacements", []); + } + // Sanity check default placements array: + if (!Array.isArray(props.get("defaultPlacements"))) { + throw new Error("Should provide an array of default placements"); + } + + if (!areaIsKnown) { + gAreas.set(aName, props); + + // Reconcile new default widgets. Have to do this before we start restoring things. + this._placeNewDefaultWidgetsInArea(aName); + + if (props.get("legacy") && !gPlacements.has(aName)) { + // Guarantee this area exists in gFuturePlacements, to avoid checking it in + // various places elsewhere. + if (!gFuturePlacements.has(aName)) { + gFuturePlacements.set(aName, new Set()); + } + } else { + this.restoreStateForArea(aName); + } + + // If we have pending build area nodes, register all of them + if (gPendingBuildAreas.has(aName)) { + let pendingNodes = gPendingBuildAreas.get(aName); + for (let [pendingNode, existingChildren] of pendingNodes) { + this.registerToolbarNode(pendingNode, existingChildren); + } + gPendingBuildAreas.delete(aName); + } + } + }, + + unregisterArea: function(aName, aDestroyPlacements) { + if (typeof aName != "string" || !/^[a-z0-9-_]{1,}$/i.test(aName)) { + throw new Error("Invalid area name"); + } + if (!gAreas.has(aName) && !gPlacements.has(aName)) { + throw new Error("Area not registered"); + } + + // Move all the widgets out + this.beginBatchUpdate(); + try { + let placements = gPlacements.get(aName); + if (placements) { + // Need to clone this array so removeWidgetFromArea doesn't modify it + placements = [...placements]; + placements.forEach(this.removeWidgetFromArea, this); + } + + // Delete all remaining traces. + gAreas.delete(aName); + // Only destroy placements when necessary: + if (aDestroyPlacements) { + gPlacements.delete(aName); + } else { + // Otherwise we need to re-set them, as removeFromArea will have emptied + // them out: + gPlacements.set(aName, placements); + } + gFuturePlacements.delete(aName); + let existingAreaNodes = gBuildAreas.get(aName); + if (existingAreaNodes) { + for (let areaNode of existingAreaNodes) { + this.notifyListeners("onAreaNodeUnregistered", aName, areaNode.customizationTarget, + CustomizableUI.REASON_AREA_UNREGISTERED); + } + } + gBuildAreas.delete(aName); + } finally { + this.endBatchUpdate(true); + } + }, + + registerToolbarNode: function(aToolbar, aExistingChildren) { + let area = aToolbar.id; + if (gBuildAreas.has(area) && gBuildAreas.get(area).has(aToolbar)) { + return; + } + let areaProperties = gAreas.get(area); + + // If this area is not registered, try to do it automatically: + if (!areaProperties) { + // If there's no defaultset attribute and this isn't a legacy extra toolbar, + // we assume that we should wait for registerArea to be called: + if (!aToolbar.hasAttribute("defaultset") && + !aToolbar.hasAttribute("customindex")) { + if (!gPendingBuildAreas.has(area)) { + gPendingBuildAreas.set(area, new Map()); + } + let pendingNodes = gPendingBuildAreas.get(area); + pendingNodes.set(aToolbar, aExistingChildren); + return; + } + let props = {type: CustomizableUI.TYPE_TOOLBAR, legacy: true}; + let defaultsetAttribute = aToolbar.getAttribute("defaultset") || ""; + props.defaultPlacements = defaultsetAttribute.split(',').filter(s => s); + this.registerArea(area, props); + areaProperties = gAreas.get(area); + } + + this.beginBatchUpdate(); + try { + let placements = gPlacements.get(area); + if (!placements && areaProperties.has("legacy")) { + let legacyState = aToolbar.getAttribute("currentset"); + if (legacyState) { + legacyState = legacyState.split(",").filter(s => s); + } + + // Manually restore the state here, so the legacy state can be converted. + this.restoreStateForArea(area, legacyState); + placements = gPlacements.get(area); + } + + // Check that the current children and the current placements match. If + // not, mark it as dirty: + if (aExistingChildren.length != placements.length || + aExistingChildren.every((id, i) => id == placements[i])) { + gDirtyAreaCache.add(area); + } + + if (areaProperties.has("overflowable")) { + aToolbar.overflowable = new OverflowableToolbar(aToolbar); + } + + this.registerBuildArea(area, aToolbar); + + // We only build the toolbar if it's been marked as "dirty". Dirty means + // one of the following things: + // 1) Items have been added, moved or removed from this toolbar before. + // 2) The number of children of the toolbar does not match the length of + // the placements array for that area. + // + // This notion of being "dirty" is stored in a cache which is persisted + // in the saved state. + if (gDirtyAreaCache.has(area)) { + this.buildArea(area, placements, aToolbar); + } + this.notifyListeners("onAreaNodeRegistered", area, aToolbar.customizationTarget); + aToolbar.setAttribute("currentset", placements.join(",")); + } finally { + this.endBatchUpdate(); + } + }, + + buildArea: function(aArea, aPlacements, aAreaNode) { + let document = aAreaNode.ownerDocument; + let window = document.defaultView; + let inPrivateWindow = PrivateBrowsingUtils.isWindowPrivate(window); + let container = aAreaNode.customizationTarget; + let areaIsPanel = gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL; + + if (!container) { + throw new Error("Expected area " + aArea + + " to have a customizationTarget attribute."); + } + + // Restore nav-bar visibility since it may have been hidden + // through a migration path (bug 938980) or an add-on. + if (aArea == CustomizableUI.AREA_NAVBAR) { + aAreaNode.collapsed = false; + } + + this.beginBatchUpdate(); + + try { + let currentNode = container.firstChild; + let placementsToRemove = new Set(); + for (let id of aPlacements) { + while (currentNode && currentNode.getAttribute("skipintoolbarset") == "true") { + currentNode = currentNode.nextSibling; + } + + if (currentNode && currentNode.id == id) { + currentNode = currentNode.nextSibling; + continue; + } + + if (this.isSpecialWidget(id) && areaIsPanel) { + placementsToRemove.add(id); + continue; + } + + let [provider, node] = this.getWidgetNode(id, window); + if (!node) { + log.debug("Unknown widget: " + id); + continue; + } + + let widget = null; + // If the placements have items in them which are (now) no longer removable, + // we shouldn't be moving them: + if (provider == CustomizableUI.PROVIDER_API) { + widget = gPalette.get(id); + if (!widget.removable && aArea != widget.defaultArea) { + placementsToRemove.add(id); + continue; + } + } else if (provider == CustomizableUI.PROVIDER_XUL && + node.parentNode != container && !this.isWidgetRemovable(node)) { + placementsToRemove.add(id); + continue; + } // Special widgets are always removable, so no need to check them + + if (inPrivateWindow && widget && !widget.showInPrivateBrowsing) { + continue; + } + + this.ensureButtonContextMenu(node, aAreaNode); + if (node.localName == "toolbarbutton") { + if (areaIsPanel) { + node.setAttribute("wrap", "true"); + } else { + node.removeAttribute("wrap"); + } + } + + // This needs updating in case we're resetting / undoing a reset. + if (widget) { + widget.currentArea = aArea; + } + this.insertWidgetBefore(node, currentNode, container, aArea); + if (gResetting) { + this.notifyListeners("onWidgetReset", node, container); + } else if (gUndoResetting) { + this.notifyListeners("onWidgetUndoMove", node, container); + } + } + + if (currentNode) { + let palette = aAreaNode.toolbox ? aAreaNode.toolbox.palette : null; + let limit = currentNode.previousSibling; + let node = container.lastChild; + while (node && node != limit) { + let previousSibling = node.previousSibling; + // Nodes opt-in to removability. If they're removable, and we haven't + // seen them in the placements array, then we toss them into the palette + // if one exists. If no palette exists, we just remove the node. If the + // node is not removable, we leave it where it is. However, we can only + // safely touch elements that have an ID - both because we depend on + // IDs, and because such elements are not intended to be widgets + // (eg, titlebar-placeholder elements). + if (node.id && node.getAttribute("skipintoolbarset") != "true") { + if (this.isWidgetRemovable(node)) { + if (palette && !this.isSpecialWidget(node.id)) { + palette.appendChild(node); + this.removeLocationAttributes(node); + } else { + container.removeChild(node); + } + } else { + node.setAttribute("removable", false); + log.debug("Adding non-removable widget to placements of " + aArea + ": " + + node.id); + gPlacements.get(aArea).push(node.id); + gDirty = true; + } + } + node = previousSibling; + } + } + + // If there are placements in here which aren't removable from their original area, + // we remove them from this area's placement array. They will (have) be(en) added + // to their original area's placements array in the block above this one. + if (placementsToRemove.size) { + let placementAry = gPlacements.get(aArea); + for (let id of placementsToRemove) { + let index = placementAry.indexOf(id); + placementAry.splice(index, 1); + } + } + + if (gResetting) { + this.notifyListeners("onAreaReset", aArea, container); + } + } finally { + this.endBatchUpdate(); + } + }, + + addPanelCloseListeners: function(aPanel) { + gELS.addSystemEventListener(aPanel, "click", this, false); + gELS.addSystemEventListener(aPanel, "keypress", this, false); + let win = aPanel.ownerGlobal; + if (!gPanelsForWindow.has(win)) { + gPanelsForWindow.set(win, new Set()); + } + gPanelsForWindow.get(win).add(this._getPanelForNode(aPanel)); + }, + + removePanelCloseListeners: function(aPanel) { + gELS.removeSystemEventListener(aPanel, "click", this, false); + gELS.removeSystemEventListener(aPanel, "keypress", this, false); + let win = aPanel.ownerGlobal; + let panels = gPanelsForWindow.get(win); + if (panels) { + panels.delete(this._getPanelForNode(aPanel)); + } + }, + + ensureButtonContextMenu: function(aNode, aAreaNode) { + const kPanelItemContextMenu = "customizationPanelItemContextMenu"; + + let currentContextMenu = aNode.getAttribute("context") || + aNode.getAttribute("contextmenu"); + let place = CustomizableUI.getPlaceForItem(aAreaNode); + let contextMenuForPlace = place == "panel" ? + kPanelItemContextMenu : + null; + if (contextMenuForPlace && !currentContextMenu) { + aNode.setAttribute("context", contextMenuForPlace); + } else if (currentContextMenu == kPanelItemContextMenu && + contextMenuForPlace != kPanelItemContextMenu) { + aNode.removeAttribute("context"); + aNode.removeAttribute("contextmenu"); + } + }, + + getWidgetProvider: function(aWidgetId) { + if (this.isSpecialWidget(aWidgetId)) { + return CustomizableUI.PROVIDER_SPECIAL; + } + if (gPalette.has(aWidgetId)) { + return CustomizableUI.PROVIDER_API; + } + // If this was an API widget that was destroyed, return null: + if (gSeenWidgets.has(aWidgetId)) { + return null; + } + + // We fall back to the XUL provider, but we don't know for sure (at this + // point) whether it exists there either. So the API is technically lying. + // Ideally, it would be able to return an error value (or throw an + // exception) if it really didn't exist. Our code calling this function + // handles that fine, but this is a public API. + return CustomizableUI.PROVIDER_XUL; + }, + + getWidgetNode: function(aWidgetId, aWindow) { + let document = aWindow.document; + + if (this.isSpecialWidget(aWidgetId)) { + let widgetNode = document.getElementById(aWidgetId) || + this.createSpecialWidget(aWidgetId, document); + return [ CustomizableUI.PROVIDER_SPECIAL, widgetNode]; + } + + let widget = gPalette.get(aWidgetId); + if (widget) { + // If we have an instance of this widget already, just use that. + if (widget.instances.has(document)) { + log.debug("An instance of widget " + aWidgetId + " already exists in this " + + "document. Reusing."); + return [ CustomizableUI.PROVIDER_API, + widget.instances.get(document) ]; + } + + return [ CustomizableUI.PROVIDER_API, + this.buildWidget(document, widget) ]; + } + + log.debug("Searching for " + aWidgetId + " in toolbox."); + let node = this.findWidgetInWindow(aWidgetId, aWindow); + if (node) { + return [ CustomizableUI.PROVIDER_XUL, node ]; + } + + log.debug("No node for " + aWidgetId + " found."); + return [null, null]; + }, + + registerMenuPanel: function(aPanelContents) { + if (gBuildAreas.has(CustomizableUI.AREA_PANEL) && + gBuildAreas.get(CustomizableUI.AREA_PANEL).has(aPanelContents)) { + return; + } + + let document = aPanelContents.ownerDocument; + + aPanelContents.toolbox = document.getElementById("navigator-toolbox"); + aPanelContents.customizationTarget = aPanelContents; + + this.addPanelCloseListeners(this._getPanelForNode(aPanelContents)); + + let placements = gPlacements.get(CustomizableUI.AREA_PANEL); + this.buildArea(CustomizableUI.AREA_PANEL, placements, aPanelContents); + this.notifyListeners("onAreaNodeRegistered", CustomizableUI.AREA_PANEL, aPanelContents); + + for (let child of aPanelContents.children) { + if (child.localName != "toolbarbutton") { + if (child.localName == "toolbaritem") { + this.ensureButtonContextMenu(child, aPanelContents); + } + continue; + } + this.ensureButtonContextMenu(child, aPanelContents); + child.setAttribute("wrap", "true"); + } + + this.registerBuildArea(CustomizableUI.AREA_PANEL, aPanelContents); + }, + + onWidgetAdded: function(aWidgetId, aArea, aPosition) { + this.insertNode(aWidgetId, aArea, aPosition, true); + + if (!gResetting) { + this._clearPreviousUIState(); + } + }, + + onWidgetRemoved: function(aWidgetId, aArea) { + let areaNodes = gBuildAreas.get(aArea); + if (!areaNodes) { + return; + } + + let area = gAreas.get(aArea); + let isToolbar = area.get("type") == CustomizableUI.TYPE_TOOLBAR; + let isOverflowable = isToolbar && area.get("overflowable"); + let showInPrivateBrowsing = gPalette.has(aWidgetId) + ? gPalette.get(aWidgetId).showInPrivateBrowsing + : true; + + for (let areaNode of areaNodes) { + let window = areaNode.ownerGlobal; + if (!showInPrivateBrowsing && + PrivateBrowsingUtils.isWindowPrivate(window)) { + continue; + } + + let container = areaNode.customizationTarget; + let widgetNode = window.document.getElementById(aWidgetId); + if (widgetNode && isOverflowable) { + container = areaNode.overflowable.getContainerFor(widgetNode); + } + + if (!widgetNode || !container.contains(widgetNode)) { + log.info("Widget " + aWidgetId + " not found, unable to remove from " + aArea); + continue; + } + + this.notifyListeners("onWidgetBeforeDOMChange", widgetNode, null, container, true); + + // We remove location attributes here to make sure they're gone too when a + // widget is removed from a toolbar to the palette. See bug 930950. + this.removeLocationAttributes(widgetNode); + // We also need to remove the panel context menu if it's there: + this.ensureButtonContextMenu(widgetNode); + widgetNode.removeAttribute("wrap"); + if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) { + container.removeChild(widgetNode); + } else { + areaNode.toolbox.palette.appendChild(widgetNode); + } + this.notifyListeners("onWidgetAfterDOMChange", widgetNode, null, container, true); + + if (isToolbar) { + areaNode.setAttribute("currentset", gPlacements.get(aArea).join(',')); + } + + let windowCache = gSingleWrapperCache.get(window); + if (windowCache) { + windowCache.delete(aWidgetId); + } + } + if (!gResetting) { + this._clearPreviousUIState(); + } + }, + + onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) { + this.insertNode(aWidgetId, aArea, aNewPosition); + if (!gResetting) { + this._clearPreviousUIState(); + } + }, + + onCustomizeEnd: function(aWindow) { + this._clearPreviousUIState(); + }, + + registerBuildArea: function(aArea, aNode) { + // We ensure that the window is registered to have its customization data + // cleaned up when unloading. + let window = aNode.ownerGlobal; + if (window.closed) { + return; + } + this.registerBuildWindow(window); + + // Also register this build area's toolbox. + if (aNode.toolbox) { + gBuildWindows.get(window).add(aNode.toolbox); + } + + if (!gBuildAreas.has(aArea)) { + gBuildAreas.set(aArea, new Set()); + } + + gBuildAreas.get(aArea).add(aNode); + + // Give a class to all customize targets to be used for styling in Customize Mode + let customizableNode = this.getCustomizeTargetForArea(aArea, window); + customizableNode.classList.add("customization-target"); + }, + + registerBuildWindow: function(aWindow) { + if (!gBuildWindows.has(aWindow)) { + gBuildWindows.set(aWindow, new Set()); + + aWindow.addEventListener("unload", this); + aWindow.addEventListener("command", this, true); + + this.notifyListeners("onWindowOpened", aWindow); + } + }, + + unregisterBuildWindow: function(aWindow) { + aWindow.removeEventListener("unload", this); + aWindow.removeEventListener("command", this, true); + gPanelsForWindow.delete(aWindow); + gBuildWindows.delete(aWindow); + gSingleWrapperCache.delete(aWindow); + let document = aWindow.document; + + for (let [areaId, areaNodes] of gBuildAreas) { + let areaProperties = gAreas.get(areaId); + for (let node of areaNodes) { + if (node.ownerDocument == document) { + this.notifyListeners("onAreaNodeUnregistered", areaId, node.customizationTarget, + CustomizableUI.REASON_WINDOW_CLOSED); + if (areaProperties.has("overflowable")) { + node.overflowable.uninit(); + node.overflowable = null; + } + areaNodes.delete(node); + } + } + } + + for (let [, widget] of gPalette) { + widget.instances.delete(document); + this.notifyListeners("onWidgetInstanceRemoved", widget.id, document); + } + + for (let [, areaMap] of gPendingBuildAreas) { + let toDelete = []; + for (let [areaNode, ] of areaMap) { + if (areaNode.ownerDocument == document) { + toDelete.push(areaNode); + } + } + for (let areaNode of toDelete) { + areaMap.delete(areaNode); + } + } + + this.notifyListeners("onWindowClosed", aWindow); + }, + + setLocationAttributes: function(aNode, aArea) { + let props = gAreas.get(aArea); + if (!props) { + throw new Error("Expected area " + aArea + " to have a properties Map " + + "associated with it."); + } + + aNode.setAttribute("cui-areatype", props.get("type") || ""); + let anchor = props.get("anchor"); + if (anchor) { + aNode.setAttribute("cui-anchorid", anchor); + } else { + aNode.removeAttribute("cui-anchorid"); + } + }, + + removeLocationAttributes: function(aNode) { + aNode.removeAttribute("cui-areatype"); + aNode.removeAttribute("cui-anchorid"); + }, + + insertNode: function(aWidgetId, aArea, aPosition, isNew) { + let areaNodes = gBuildAreas.get(aArea); + if (!areaNodes) { + return; + } + + let placements = gPlacements.get(aArea); + if (!placements) { + log.error("Could not find any placements for " + aArea + + " when moving a widget."); + return; + } + + // Go through each of the nodes associated with this area and move the + // widget to the requested location. + for (let areaNode of areaNodes) { + this.insertNodeInWindow(aWidgetId, areaNode, isNew); + } + }, + + insertNodeInWindow: function(aWidgetId, aAreaNode, isNew) { + let window = aAreaNode.ownerGlobal; + let showInPrivateBrowsing = gPalette.has(aWidgetId) + ? gPalette.get(aWidgetId).showInPrivateBrowsing + : true; + + if (!showInPrivateBrowsing && PrivateBrowsingUtils.isWindowPrivate(window)) { + return; + } + + let [, widgetNode] = this.getWidgetNode(aWidgetId, window); + if (!widgetNode) { + log.error("Widget '" + aWidgetId + "' not found, unable to move"); + return; + } + + let areaId = aAreaNode.id; + if (isNew) { + this.ensureButtonContextMenu(widgetNode, aAreaNode); + if (widgetNode.localName == "toolbarbutton" && areaId == CustomizableUI.AREA_PANEL) { + widgetNode.setAttribute("wrap", "true"); + } + } + + let [insertionContainer, nextNode] = this.findInsertionPoints(widgetNode, aAreaNode); + this.insertWidgetBefore(widgetNode, nextNode, insertionContainer, areaId); + + if (gAreas.get(areaId).get("type") == CustomizableUI.TYPE_TOOLBAR) { + aAreaNode.setAttribute("currentset", gPlacements.get(areaId).join(',')); + } + }, + + findInsertionPoints: function(aNode, aAreaNode) { + let areaId = aAreaNode.id; + let props = gAreas.get(areaId); + + // For overflowable toolbars, rely on them (because the work is more complicated): + if (props.get("type") == CustomizableUI.TYPE_TOOLBAR && props.get("overflowable")) { + return aAreaNode.overflowable.findOverflowedInsertionPoints(aNode); + } + + let container = aAreaNode.customizationTarget; + let placements = gPlacements.get(areaId); + let nodeIndex = placements.indexOf(aNode.id); + + while (++nodeIndex < placements.length) { + let nextNodeId = placements[nodeIndex]; + let nextNode = container.getElementsByAttribute("id", nextNodeId).item(0); + + if (nextNode) { + return [container, nextNode]; + } + } + + return [container, null]; + }, + + insertWidgetBefore: function(aNode, aNextNode, aContainer, aArea) { + this.notifyListeners("onWidgetBeforeDOMChange", aNode, aNextNode, aContainer); + this.setLocationAttributes(aNode, aArea); + aContainer.insertBefore(aNode, aNextNode); + this.notifyListeners("onWidgetAfterDOMChange", aNode, aNextNode, aContainer); + }, + + handleEvent: function(aEvent) { + switch (aEvent.type) { + case "command": + if (!this._originalEventInPanel(aEvent)) { + break; + } + aEvent = aEvent.sourceEvent; + // Fall through + case "click": + case "keypress": + this.maybeAutoHidePanel(aEvent); + break; + case "unload": + this.unregisterBuildWindow(aEvent.currentTarget); + break; + } + }, + + _originalEventInPanel: function(aEvent) { + let e = aEvent.sourceEvent; + if (!e) { + return false; + } + let node = this._getPanelForNode(e.target); + if (!node) { + return false; + } + let win = e.view; + let panels = gPanelsForWindow.get(win); + return !!panels && panels.has(node); + }, + + isSpecialWidget: function(aId) { + return (aId.startsWith(kSpecialWidgetPfx) || + aId.startsWith("separator") || + aId.startsWith("spring") || + aId.startsWith("spacer")); + }, + + ensureSpecialWidgetId: function(aId) { + let nodeType = aId.match(/spring|spacer|separator/)[0]; + // If the ID we were passed isn't a generated one, generate one now: + if (nodeType == aId) { + // Ids are differentiated through a unique count suffix. + return kSpecialWidgetPfx + aId + (++gNewElementCount); + } + return aId; + }, + + createSpecialWidget: function(aId, aDocument) { + let nodeName = "toolbar" + aId.match(/spring|spacer|separator/)[0]; + let node = aDocument.createElementNS(kNSXUL, nodeName); + node.id = this.ensureSpecialWidgetId(aId); + if (nodeName == "toolbarspring") { + node.flex = 1; + } + return node; + }, + + /* Find a XUL-provided widget in a window. Don't try to use this + * for an API-provided widget or a special widget. + */ + findWidgetInWindow: function(aId, aWindow) { + if (!gBuildWindows.has(aWindow)) { + throw new Error("Build window not registered"); + } + + if (!aId) { + log.error("findWidgetInWindow was passed an empty string."); + return null; + } + + let document = aWindow.document; + + // look for a node with the same id, as the node may be + // in a different toolbar. + let node = document.getElementById(aId); + if (node) { + let parent = node.parentNode; + while (parent && !(parent.customizationTarget || + parent == aWindow.gNavToolbox.palette)) { + parent = parent.parentNode; + } + + if (parent) { + let nodeInArea = node.parentNode.localName == "toolbarpaletteitem" ? + node.parentNode : node; + // Check if we're in a customization target, or in the palette: + if ((parent.customizationTarget == nodeInArea.parentNode && + gBuildWindows.get(aWindow).has(parent.toolbox)) || + aWindow.gNavToolbox.palette == nodeInArea.parentNode) { + // Normalize the removable attribute. For backwards compat, if + // the widget is not located in a toolbox palette then absence + // of the "removable" attribute means it is not removable. + if (!node.hasAttribute("removable")) { + // If we first see this in customization mode, it may be in the + // customization palette instead of the toolbox palette. + node.setAttribute("removable", !parent.customizationTarget); + } + return node; + } + } + } + + let toolboxes = gBuildWindows.get(aWindow); + for (let toolbox of toolboxes) { + if (toolbox.palette) { + // Attempt to locate a node with a matching ID within + // the palette. + let node = toolbox.palette.getElementsByAttribute("id", aId)[0]; + if (node) { + // Normalize the removable attribute. For backwards compat, this + // is optional if the widget is located in the toolbox palette, + // and defaults to *true*, unlike if it was located elsewhere. + if (!node.hasAttribute("removable")) { + node.setAttribute("removable", true); + } + return node; + } + } + } + return null; + }, + + buildWidget: function(aDocument, aWidget) { + if (aDocument.documentURI != kExpectedWindowURL) { + throw new Error("buildWidget was called for a non-browser window!"); + } + if (typeof aWidget == "string") { + aWidget = gPalette.get(aWidget); + } + if (!aWidget) { + throw new Error("buildWidget was passed a non-widget to build."); + } + + log.debug("Building " + aWidget.id + " of type " + aWidget.type); + + let node; + if (aWidget.type == "custom") { + if (aWidget.onBuild) { + node = aWidget.onBuild(aDocument); + } + if (!node || !(node instanceof aDocument.defaultView.XULElement)) + log.error("Custom widget with id " + aWidget.id + " does not return a valid node"); + } + else { + if (aWidget.onBeforeCreated) { + aWidget.onBeforeCreated(aDocument); + } + node = aDocument.createElementNS(kNSXUL, "toolbarbutton"); + + node.setAttribute("id", aWidget.id); + node.setAttribute("widget-id", aWidget.id); + node.setAttribute("widget-type", aWidget.type); + if (aWidget.disabled) { + node.setAttribute("disabled", true); + } + node.setAttribute("removable", aWidget.removable); + node.setAttribute("overflows", aWidget.overflows); + if (aWidget.tabSpecific) { + node.setAttribute("tabspecific", aWidget.tabSpecific); + } + node.setAttribute("label", this.getLocalizedProperty(aWidget, "label")); + let additionalTooltipArguments = []; + if (aWidget.shortcutId) { + let keyEl = aDocument.getElementById(aWidget.shortcutId); + if (keyEl) { + additionalTooltipArguments.push(ShortcutUtils.prettifyShortcut(keyEl)); + } else { + log.error("Key element with id '" + aWidget.shortcutId + "' for widget '" + aWidget.id + + "' not found!"); + } + } + + let tooltip = this.getLocalizedProperty(aWidget, "tooltiptext", additionalTooltipArguments); + if (tooltip) { + node.setAttribute("tooltiptext", tooltip); + } + node.setAttribute("class", "toolbarbutton-1 chromeclass-toolbar-additional"); + + let commandHandler = this.handleWidgetCommand.bind(this, aWidget, node); + node.addEventListener("command", commandHandler, false); + let clickHandler = this.handleWidgetClick.bind(this, aWidget, node); + node.addEventListener("click", clickHandler, false); + + // If the widget has a view, and has view showing / hiding listeners, + // hook those up to this widget. + if (aWidget.type == "view") { + log.debug("Widget " + aWidget.id + " has a view. Auto-registering event handlers."); + let viewNode = aDocument.getElementById(aWidget.viewId); + + if (viewNode) { + // PanelUI relies on the .PanelUI-subView class to be able to show only + // one sub-view at a time. + viewNode.classList.add("PanelUI-subView"); + + for (let eventName of kSubviewEvents) { + let handler = "on" + eventName; + if (typeof aWidget[handler] == "function") { + viewNode.addEventListener(eventName, aWidget[handler], false); + } + } + + log.debug("Widget " + aWidget.id + " showing and hiding event handlers set."); + } else { + log.error("Could not find the view node with id: " + aWidget.viewId + + ", for widget: " + aWidget.id + "."); + } + } + + if (aWidget.onCreated) { + aWidget.onCreated(node); + } + } + + aWidget.instances.set(aDocument, node); + return node; + }, + + getLocalizedProperty: function(aWidget, aProp, aFormatArgs, aDef) { + const kReqStringProps = ["label"]; + + if (typeof aWidget == "string") { + aWidget = gPalette.get(aWidget); + } + if (!aWidget) { + throw new Error("getLocalizedProperty was passed a non-widget to work with."); + } + let def, name; + // Let widgets pass their own string identifiers or strings, so that + // we can use strings which aren't the default (in case string ids change) + // and so that non-builtin-widgets can also provide labels, tooltips, etc. + if (aWidget[aProp] != null) { + name = aWidget[aProp]; + // By using this as the default, if a widget provides a full string rather + // than a string ID for localization, we will fall back to that string + // and return that. + def = aDef || name; + } else { + name = aWidget.id + "." + aProp; + def = aDef || ""; + } + try { + if (Array.isArray(aFormatArgs) && aFormatArgs.length) { + return gWidgetsBundle.formatStringFromName(name, aFormatArgs, + aFormatArgs.length) || def; + } + return gWidgetsBundle.GetStringFromName(name) || def; + } catch (ex) { + // If an empty string was explicitly passed, treat it as an actual + // value rather than a missing property. + if (!def && (name != "" || kReqStringProps.includes(aProp))) { + log.error("Could not localize property '" + name + "'."); + } + } + return def; + }, + + addShortcut: function(aShortcutNode, aTargetNode) { + if (!aTargetNode) + aTargetNode = aShortcutNode; + let document = aShortcutNode.ownerDocument; + + // Detect if we've already been here before. + if (!aTargetNode || aTargetNode.hasAttribute("shortcut")) + return; + + let shortcutId = aShortcutNode.getAttribute("key"); + let shortcut; + if (shortcutId) { + shortcut = document.getElementById(shortcutId); + } else { + let commandId = aShortcutNode.getAttribute("command"); + if (commandId) + shortcut = ShortcutUtils.findShortcut(document.getElementById(commandId)); + } + if (!shortcut) { + return; + } + + aTargetNode.setAttribute("shortcut", ShortcutUtils.prettifyShortcut(shortcut)); + }, + + handleWidgetCommand: function(aWidget, aNode, aEvent) { + log.debug("handleWidgetCommand"); + + if (aWidget.type == "button") { + if (aWidget.onCommand) { + try { + aWidget.onCommand.call(null, aEvent); + } catch (e) { + log.error(e); + } + } else { + // XXXunf Need to think this through more, and formalize. + Services.obs.notifyObservers(aNode, + "customizedui-widget-command", + aWidget.id); + } + } else if (aWidget.type == "view") { + let ownerWindow = aNode.ownerGlobal; + let area = this.getPlacementOfWidget(aNode.id).area; + let anchor = aNode; + if (area != CustomizableUI.AREA_PANEL) { + let wrapper = this.wrapWidget(aWidget.id).forWindow(ownerWindow); + if (wrapper && wrapper.anchor) { + this.hidePanelForNode(aNode); + anchor = wrapper.anchor; + } + } + ownerWindow.PanelUI.showSubView(aWidget.viewId, anchor, area); + } + }, + + handleWidgetClick: function(aWidget, aNode, aEvent) { + log.debug("handleWidgetClick"); + if (aWidget.onClick) { + try { + aWidget.onClick.call(null, aEvent); + } catch (e) { + Cu.reportError(e); + } + } else { + // XXXunf Need to think this through more, and formalize. + Services.obs.notifyObservers(aNode, "customizedui-widget-click", aWidget.id); + } + }, + + _getPanelForNode: function(aNode) { + let panel = aNode; + while (panel && panel.localName != "panel") + panel = panel.parentNode; + return panel; + }, + + /* + * If people put things in the panel which need more than single-click interaction, + * we don't want to close it. Right now we check for text inputs and menu buttons. + * We also check for being outside of any toolbaritem/toolbarbutton, ie on a blank + * part of the menu. + */ + _isOnInteractiveElement: function(aEvent) { + function getMenuPopupForDescendant(aNode) { + let lastPopup = null; + while (aNode && aNode.parentNode && + aNode.parentNode.localName.startsWith("menu")) { + lastPopup = aNode.localName == "menupopup" ? aNode : lastPopup; + aNode = aNode.parentNode; + } + return lastPopup; + } + + let target = aEvent.originalTarget; + let panel = this._getPanelForNode(aEvent.currentTarget); + // This can happen in e.g. customize mode. If there's no panel, + // there's clearly nothing for us to close; pretend we're interactive. + if (!panel) { + return true; + } + // We keep track of: + // whether we're in an input container (text field) + let inInput = false; + // whether we're in a popup/context menu + let inMenu = false; + // whether we're in a toolbarbutton/toolbaritem + let inItem = false; + // whether the current menuitem has a valid closemenu attribute + let menuitemCloseMenu = "auto"; + // whether the toolbarbutton/item has a valid closemenu attribute. + let closemenu = "auto"; + + // While keeping track of that, we go from the original target back up, + // to the panel if we have to. We bail as soon as we find an input, + // a toolbarbutton/item, or the panel: + while (true && target) { + // Skip out of iframes etc: + if (target.nodeType == target.DOCUMENT_NODE) { + if (!target.defaultView) { + // Err, we're done. + break; + } + // Cue some voodoo + target = target.defaultView.QueryInterface(Ci.nsIInterfaceRequestor) + .getInterface(Ci.nsIWebNavigation) + .QueryInterface(Ci.nsIDocShell) + .chromeEventHandler; + if (!target) { + break; + } + } + let tagName = target.localName; + inInput = tagName == "input" || tagName == "textbox"; + inItem = tagName == "toolbaritem" || tagName == "toolbarbutton"; + let isMenuItem = tagName == "menuitem"; + inMenu = inMenu || isMenuItem; + if (inItem && target.hasAttribute("closemenu")) { + let closemenuVal = target.getAttribute("closemenu"); + closemenu = (closemenuVal == "single" || closemenuVal == "none") ? + closemenuVal : "auto"; + } + + if (isMenuItem && target.hasAttribute("closemenu")) { + let closemenuVal = target.getAttribute("closemenu"); + menuitemCloseMenu = (closemenuVal == "single" || closemenuVal == "none") ? + closemenuVal : "auto"; + } + // Break out of the loop immediately for disabled items, as we need to + // keep the menu open in that case. + if (target.getAttribute("disabled") == "true") { + return true; + } + + // This isn't in the loop condition because we want to break before + // changing |target| if any of these conditions are true + if (inInput || inItem || target == panel) { + break; + } + // We need specific code for popups: the item on which they were invoked + // isn't necessarily in their parentNode chain: + if (isMenuItem) { + let topmostMenuPopup = getMenuPopupForDescendant(target); + target = (topmostMenuPopup && topmostMenuPopup.triggerNode) || + target.parentNode; + } else { + target = target.parentNode; + } + } + + // If the user clicked a menu item... + if (inMenu) { + // We care if we're in an input also, + // or if the user specified closemenu!="auto": + if (inInput || menuitemCloseMenu != "auto") { + return true; + } + // Otherwise, we're probably fine to close the panel + return false; + } + // If we're not in a menu, and we *are* in a type="menu" toolbarbutton, + // we'll now interact with the menu + if (inItem && target.getAttribute("type") == "menu") { + return true; + } + // If we're not in a menu, and we *are* in a type="menu-button" toolbarbutton, + // it depends whether we're in the dropmarker or the 'real' button: + if (inItem && target.getAttribute("type") == "menu-button") { + // 'real' button (which has a single action): + if (target.getAttribute("anonid") == "button") { + return closemenu != "none"; + } + // otherwise, this is the outer button, and the user will now + // interact with the menu: + return true; + } + return inInput || !inItem; + }, + + hidePanelForNode: function(aNode) { + let panel = this._getPanelForNode(aNode); + if (panel) { + panel.hidePopup(); + } + }, + + maybeAutoHidePanel: function(aEvent) { + if (aEvent.type == "keypress") { + if (aEvent.keyCode != aEvent.DOM_VK_RETURN) { + return; + } + // If the user hit enter/return, we don't check preventDefault - it makes sense + // that this was prevented, but we probably still want to close the panel. + // If consumers don't want this to happen, they should specify the closemenu + // attribute. + + } else if (aEvent.type != "command") { // mouse events: + if (aEvent.defaultPrevented || aEvent.button != 0) { + return; + } + let isInteractive = this._isOnInteractiveElement(aEvent); + log.debug("maybeAutoHidePanel: interactive ? " + isInteractive); + if (isInteractive) { + return; + } + } + + // We can't use event.target because we might have passed a panelview + // anonymous content boundary as well, and so target points to the + // panelmultiview in that case. Unfortunately, this means we get + // anonymous child nodes instead of the real ones, so looking for the + // 'stoooop, don't close me' attributes is more involved. + let target = aEvent.originalTarget; + let closemenu = "auto"; + let widgetType = "button"; + while (target.parentNode && target.localName != "panel") { + closemenu = target.getAttribute("closemenu"); + widgetType = target.getAttribute("widget-type"); + if (closemenu == "none" || closemenu == "single" || + widgetType == "view") { + break; + } + target = target.parentNode; + } + if (closemenu == "none" || widgetType == "view") { + return; + } + + if (closemenu == "single") { + let panel = this._getPanelForNode(target); + let multiview = panel.querySelector("panelmultiview"); + if (multiview.showingSubView) { + multiview.showMainView(); + return; + } + } + + // If we get here, we can actually hide the popup: + this.hidePanelForNode(aEvent.target); + }, + + getUnusedWidgets: function(aWindowPalette) { + let window = aWindowPalette.ownerGlobal; + let isWindowPrivate = PrivateBrowsingUtils.isWindowPrivate(window); + // We use a Set because there can be overlap between the widgets in + // gPalette and the items in the palette, especially after the first + // customization, since programmatically generated widgets will remain + // in the toolbox palette. + let widgets = new Set(); + + // It's possible that some widgets have been defined programmatically and + // have not been overlayed into the palette. We can find those inside + // gPalette. + for (let [id, widget] of gPalette) { + if (!widget.currentArea) { + if (widget.showInPrivateBrowsing || !isWindowPrivate) { + widgets.add(id); + } + } + } + + log.debug("Iterating the actual nodes of the window palette"); + for (let node of aWindowPalette.children) { + log.debug("In palette children: " + node.id); + if (node.id && !this.getPlacementOfWidget(node.id)) { + widgets.add(node.id); + } + } + + return [...widgets]; + }, + + getPlacementOfWidget: function(aWidgetId, aOnlyRegistered, aDeadAreas) { + if (aOnlyRegistered && !this.widgetExists(aWidgetId)) { + return null; + } + + for (let [area, placements] of gPlacements) { + if (!gAreas.has(area) && !aDeadAreas) { + continue; + } + let index = placements.indexOf(aWidgetId); + if (index != -1) { + return { area: area, position: index }; + } + } + + return null; + }, + + widgetExists: function(aWidgetId) { + if (gPalette.has(aWidgetId) || this.isSpecialWidget(aWidgetId)) { + return true; + } + + // Destroyed API widgets are in gSeenWidgets, but not in gPalette: + if (gSeenWidgets.has(aWidgetId)) { + return false; + } + + // We're assuming XUL widgets always exist, as it's much harder to check, + // and checking would be much more error prone. + return true; + }, + + addWidgetToArea: function(aWidgetId, aArea, aPosition, aInitialAdd) { + if (!gAreas.has(aArea)) { + throw new Error("Unknown customization area: " + aArea); + } + + // Hack: don't want special widgets in the panel (need to check here as well + // as in canWidgetMoveToArea because the menu panel is lazy): + if (gAreas.get(aArea).get("type") == CustomizableUI.TYPE_MENU_PANEL && + this.isSpecialWidget(aWidgetId)) { + return; + } + + // If this is a lazy area that hasn't been restored yet, we can't yet modify + // it - would would at least like to add to it. So we keep track of it in + // gFuturePlacements, and use that to add it when restoring the area. We + // throw away aPosition though, as that can only be bogus if the area hasn't + // yet been restorted (caller can't possibly know where its putting the + // widget in relation to other widgets). + if (this.isAreaLazy(aArea)) { + gFuturePlacements.get(aArea).add(aWidgetId); + return; + } + + if (this.isSpecialWidget(aWidgetId)) { + aWidgetId = this.ensureSpecialWidgetId(aWidgetId); + } + + let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true); + if (oldPlacement && oldPlacement.area == aArea) { + this.moveWidgetWithinArea(aWidgetId, aPosition); + return; + } + + // Do nothing if the widget is not allowed to move to the target area. + if (!this.canWidgetMoveToArea(aWidgetId, aArea)) { + return; + } + + if (oldPlacement) { + this.removeWidgetFromArea(aWidgetId); + } + + if (!gPlacements.has(aArea)) { + gPlacements.set(aArea, [aWidgetId]); + aPosition = 0; + } else { + let placements = gPlacements.get(aArea); + if (typeof aPosition != "number") { + aPosition = placements.length; + } + if (aPosition < 0) { + aPosition = 0; + } + placements.splice(aPosition, 0, aWidgetId); + } + + let widget = gPalette.get(aWidgetId); + if (widget) { + widget.currentArea = aArea; + widget.currentPosition = aPosition; + } + + // We initially set placements with addWidgetToArea, so in that case + // we don't consider the area "dirtied". + if (!aInitialAdd) { + gDirtyAreaCache.add(aArea); + } + + gDirty = true; + this.saveState(); + + this.notifyListeners("onWidgetAdded", aWidgetId, aArea, aPosition); + }, + + removeWidgetFromArea: function(aWidgetId) { + let oldPlacement = this.getPlacementOfWidget(aWidgetId, false, true); + if (!oldPlacement) { + return; + } + + if (!this.isWidgetRemovable(aWidgetId)) { + return; + } + + let placements = gPlacements.get(oldPlacement.area); + let position = placements.indexOf(aWidgetId); + if (position != -1) { + placements.splice(position, 1); + } + + let widget = gPalette.get(aWidgetId); + if (widget) { + widget.currentArea = null; + widget.currentPosition = null; + } + + gDirty = true; + this.saveState(); + gDirtyAreaCache.add(oldPlacement.area); + + this.notifyListeners("onWidgetRemoved", aWidgetId, oldPlacement.area); + }, + + moveWidgetWithinArea: function(aWidgetId, aPosition) { + let oldPlacement = this.getPlacementOfWidget(aWidgetId); + if (!oldPlacement) { + return; + } + + let placements = gPlacements.get(oldPlacement.area); + if (typeof aPosition != "number") { + aPosition = placements.length; + } else if (aPosition < 0) { + aPosition = 0; + } else if (aPosition > placements.length) { + aPosition = placements.length; + } + + let widget = gPalette.get(aWidgetId); + if (widget) { + widget.currentPosition = aPosition; + widget.currentArea = oldPlacement.area; + } + + if (aPosition == oldPlacement.position) { + return; + } + + placements.splice(oldPlacement.position, 1); + // If we just removed the item from *before* where it is now added, + // we need to compensate the position offset for that: + if (oldPlacement.position < aPosition) { + aPosition--; + } + placements.splice(aPosition, 0, aWidgetId); + + gDirty = true; + gDirtyAreaCache.add(oldPlacement.area); + + this.saveState(); + + this.notifyListeners("onWidgetMoved", aWidgetId, oldPlacement.area, + oldPlacement.position, aPosition); + }, + + // Note that this does not populate gPlacements, which is done lazily so that + // the legacy state can be migrated, which is only available once a browser + // window is openned. + // The panel area is an exception here, since it has no legacy state and is + // built lazily - and therefore wouldn't otherwise result in restoring its + // state immediately when a browser window opens, which is important for + // other consumers of this API. + loadSavedState: function() { + let state = 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 new file mode 100644 index 000000000..907e2e0f7 --- /dev/null +++ b/browser/components/customizableui/CustomizableWidgets.jsm @@ -0,0 +1,1281 @@ +/* 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: "social-share-button", + // custom build our button so we can attach to the share command + type: "custom", + onBuild: function(aDocument) { + let node = aDocument.createElementNS(kNSXUL, "toolbarbutton"); + node.setAttribute("id", this.id); + node.classList.add("toolbarbutton-1"); + node.classList.add("chromeclass-toolbar-additional"); + node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label")); + node.setAttribute("tooltiptext", CustomizableUI.getLocalizedProperty(this, "tooltiptext")); + node.setAttribute("removable", "true"); + node.setAttribute("observes", "Social:PageShareable"); + node.setAttribute("command", "Social:SharePage"); + + let listener = { + onWidgetAdded: (aWidgetId) => { + if (aWidgetId != this.id) + return; + + Services.obs.notifyObservers(null, "social:" + this.id + "-added", null); + }, + + onWidgetRemoved: aWidgetId => { + if (aWidgetId != this.id) + return; + + Services.obs.notifyObservers(null, "social:" + this.id + "-removed", null); + }, + + onWidgetInstanceRemoved: (aWidgetId, aDoc) => { + if (aWidgetId != this.id || aDoc != aDocument) + return; + + CustomizableUI.removeListener(listener); + } + }; + CustomizableUI.addListener(listener); + + return node; + } + }, { + 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 == "win") { + preferencesButton.label = "preferences-button.labelWin"; + preferencesButton.tooltiptext = "preferences-button.tooltipWin2"; +} else 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); + }, + }); +} + +if (AppConstants.E10S_TESTING_ONLY) { + if (Services.appinfo.browserTabsRemoteAutostart) { + CustomizableWidgets.push({ + id: "e10s-button", + defaultArea: CustomizableUI.AREA_PANEL, + onBuild: function(aDocument) { + let node = aDocument.createElementNS(kNSXUL, "toolbarbutton"); + node.setAttribute("label", CustomizableUI.getLocalizedProperty(this, "label")); + node.setAttribute("tooltiptext", CustomizableUI.getLocalizedProperty(this, "tooltiptext")); + }, + onCommand: function(aEvent) { + let win = aEvent.view; + win.OpenBrowserWindow({remote: false}); + }, + }); + } +} diff --git a/browser/components/customizableui/CustomizeMode.jsm b/browser/components/customizableui/CustomizeMode.jsm new file mode 100644 index 000000000..49868cdbd --- /dev/null +++ b/browser/components/customizableui/CustomizeMode.jsm @@ -0,0 +1,2341 @@ +/* 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"); + this.tipPanel = this.document.getElementById("customization-tipPanel"); + 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.maybeShowTip(panelHolder); + + 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.hideTip(); + + 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, '\\"') + "\")"; + }, + + maybeShowTip: function(aAnchor) { + let shown = false; + const kShownPref = "browser.customizemode.tip0.shown"; + try { + shown = Services.prefs.getBoolPref(kShownPref); + } catch (ex) {} + if (shown) + return; + + let anchorNode = aAnchor || this.document.getElementById("customization-panelHolder"); + let messageNode = this.tipPanel.querySelector(".customization-tipPanel-contentMessage"); + if (!messageNode.childElementCount) { + // Put the tip contents in the popup. + let bundle = this.document.getElementById("bundle_browser"); + const kLabelClass = "customization-tipPanel-link"; + messageNode.innerHTML = bundle.getFormattedString("customizeTips.tip0", [ + "<label class=\"customization-tipPanel-em\" value=\"" + + bundle.getString("customizeTips.tip0.hint") + "\"/>", + this.document.getElementById("bundle_brand").getString("brandShortName"), + "<label class=\"" + kLabelClass + " text-link\" value=\"" + + bundle.getString("customizeTips.tip0.learnMore") + "\"/>" + ]); + + messageNode.querySelector("." + kLabelClass).addEventListener("click", () => { + let url = Services.urlFormatter.formatURLPref("browser.customizemode.tip0.learnMoreUrl"); + let browser = this.browser; + browser.selectedTab = browser.addTab(url); + this.hideTip(); + }); + } + + this.tipPanel.hidden = false; + this.tipPanel.openPopup(anchorNode); + Services.prefs.setBoolPref(kShownPref, true); + }, + + hideTip: function() { + this.tipPanel.hidePopup(); + }, + + _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 new file mode 100644 index 000000000..1b4eb59dc --- /dev/null +++ b/browser/components/customizableui/DragPositionManager.jsm @@ -0,0 +1,420 @@ +/* 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 new file mode 100644 index 000000000..768cebbca --- /dev/null +++ b/browser/components/customizableui/PanelWideWidgetTracker.jsm @@ -0,0 +1,172 @@ +/* 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 new file mode 100644 index 000000000..44736e4c4 --- /dev/null +++ b/browser/components/customizableui/ScrollbarSampler.jsm @@ -0,0 +1,65 @@ +/* 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 new file mode 100644 index 000000000..b665630a2 --- /dev/null +++ b/browser/components/customizableui/content/customizeMode.inc.xul @@ -0,0 +1,82 @@ +<!-- 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 new file mode 100644 index 000000000..05c0112cd --- /dev/null +++ b/browser/components/customizableui/content/jar.mn @@ -0,0 +1,10 @@ +# 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 new file mode 100644 index 000000000..eb4454d28 --- /dev/null +++ b/browser/components/customizableui/content/moz.build @@ -0,0 +1,7 @@ +# -*- 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 new file mode 100644 index 000000000..ba44636f1 --- /dev/null +++ b/browser/components/customizableui/content/panelUI.css @@ -0,0 +1,31 @@ +/* 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 new file mode 100644 index 000000000..1b8fc0236 --- /dev/null +++ b/browser/components/customizableui/content/panelUI.inc.xul @@ -0,0 +1,407 @@ +<!-- 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-socialapi" flex="1"/> + + <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="customization-tipPanel" + type="arrow" + flip="none" + side="left" + position="leftcenter topright" + noautohide="true" + hidden="true"> + <hbox class="customization-tipPanel-wrapper"> + <vbox class="customization-tipPanel-infoBox"/> + <vbox class="customization-tipPanel-content" flex="1"> + <description class="customization-tipPanel-contentMessage"/> + <image class="customization-tipPanel-contentImage"/> + </vbox> + <vbox pack="start" align="end" class="customization-tipPanel-closeBox"> + <toolbarbutton oncommand="gCustomizeMode.hideTip()" class="close-icon"/> + </vbox> + </hbox> +</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 new file mode 100644 index 000000000..66fa0c184 --- /dev/null +++ b/browser/components/customizableui/content/panelUI.js @@ -0,0 +1,558 @@ +/* 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 new file mode 100644 index 000000000..6893bd8ff --- /dev/null +++ b/browser/components/customizableui/content/panelUI.xml @@ -0,0 +1,509 @@ +<?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 new file mode 100644 index 000000000..4e6964c9f --- /dev/null +++ b/browser/components/customizableui/content/toolbar.xml @@ -0,0 +1,618 @@ +<?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 new file mode 100644 index 000000000..72ec391d8 --- /dev/null +++ b/browser/components/customizableui/moz.build @@ -0,0 +1,26 @@ +# -*- 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', +] + +BROWSER_CHROME_MANIFESTS += ['test/browser.ini'] + +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 + +with Files('**'): + BUG_COMPONENT = ('Firefox', 'Toolbars and Customization') diff --git a/browser/components/customizableui/test/.eslintrc.js b/browser/components/customizableui/test/.eslintrc.js new file mode 100644 index 000000000..c764b133d --- /dev/null +++ b/browser/components/customizableui/test/.eslintrc.js @@ -0,0 +1,7 @@ +"use strict"; + +module.exports = { + "extends": [ + "../../../../testing/mochitest/browser.eslintrc.js" + ] +}; diff --git a/browser/components/customizableui/test/browser.ini b/browser/components/customizableui/test/browser.ini new file mode 100644 index 000000000..1c1f30498 --- /dev/null +++ b/browser/components/customizableui/test/browser.ini @@ -0,0 +1,154 @@ +[DEFAULT] +support-files = + head.js + support/test_967000_charEncoding_page.html + support/feeds_test_page.html + support/test-feed.xml + +[browser_873501_handle_specials.js] +[browser_876926_customize_mode_wrapping.js] +[browser_876944_customize_mode_create_destroy.js] +[browser_877006_missing_view.js] +[browser_877178_unregisterArea.js] +[browser_877447_skip_missing_ids.js] +[browser_878452_drag_to_panel.js] +[browser_880164_customization_context_menus.js] +[browser_880382_drag_wide_widgets_in_panel.js] +[browser_884402_customize_from_overflow.js] +skip-if = os == "linux" +[browser_885052_customize_mode_observers_disabed.js] +tags = fullscreen +# Bug 951403 - Disabled on OSX for frequent failures +skip-if = os == "mac" + +[browser_885530_showInPrivateBrowsing.js] +[browser_886323_buildArea_removable_nodes.js] +[browser_887438_currentset_shim.js] +[browser_888817_currentset_updating.js] +[browser_890140_orphaned_placeholders.js] +[browser_890262_destroyWidget_after_add_to_panel.js] +[browser_892955_isWidgetRemovable_for_removed_widgets.js] +[browser_892956_destroyWidget_defaultPlacements.js] +[browser_909779_overflow_toolbars_new_window.js] +skip-if = os == "linux" + +[browser_901207_searchbar_in_panel.js] +[browser_913972_currentset_overflow.js] +skip-if = os == "linux" + +[browser_914138_widget_API_overflowable_toolbar.js] +skip-if = os == "linux" + +[browser_914863_disabled_help_quit_buttons.js] +[browser_918049_skipintoolbarset_dnd.js] +[browser_923857_customize_mode_event_wrapping_during_reset.js] +[browser_927717_customize_drag_empty_toolbar.js] + +# Bug 1163231 - Causes failures on Developer Edition on Windows 7. +# [browser_932928_show_notice_when_palette_empty.js] + +[browser_934113_menubar_removable.js] +# Because this test is about the menubar, it can't be run on mac +skip-if = os == "mac" + +[browser_934951_zoom_in_toolbar.js] +[browser_938980_navbar_collapsed.js] +[browser_938995_indefaultstate_nonremovable.js] +[browser_940013_registerToolbarNode_calls_registerArea.js] +[browser_940307_panel_click_closure_handling.js] +[browser_940946_removable_from_navbar_customizemode.js] +[browser_941083_invalidate_wrapper_cache_createWidget.js] +[browser_942581_unregisterArea_keeps_placements.js] +[browser_943683_migration_test.js] +[browser_944887_destroyWidget_should_destroy_in_palette.js] +[browser_945739_showInPrivateBrowsing_customize_mode.js] +[browser_947914_button_addons.js] +skip-if = os == "linux" # Intermittent failures +[browser_947914_button_copy.js] +subsuite = clipboard +skip-if = os == "linux" # Intermittent failures on Linux +[browser_947914_button_cut.js] +subsuite = clipboard +skip-if = os == "linux" # Intermittent failures on Linux +[browser_947914_button_find.js] +skip-if = os == "linux" # Intermittent failures +[browser_947914_button_history.js] +skip-if = os == "linux" # Intermittent failures +[browser_947914_button_newPrivateWindow.js] +skip-if = os == "linux" # Intermittent failures +[browser_947914_button_newWindow.js] +skip-if = os == "linux" # Intermittent failures +[browser_947914_button_paste.js] +subsuite = clipboard +skip-if = os == "linux" # Intermittent failures on Linux +[browser_947914_button_print.js] +skip-if = os == "linux" # Intermittent failures on Linux +[browser_947914_button_savePage.js] +skip-if = os == "linux" # Intermittent failures +[browser_947914_button_zoomIn.js] +skip-if = os == "linux" # Intermittent failures +[browser_947914_button_zoomOut.js] +skip-if = os == "linux" # Intermittent failures +[browser_947914_button_zoomReset.js] +skip-if = os == "linux" # Intermittent failures +[browser_947987_removable_default.js] +[browser_948985_non_removable_defaultArea.js] +[browser_952963_areaType_getter_no_area.js] +[browser_956602_remove_special_widget.js] +[browser_962069_drag_to_overflow_chevron.js] +[browser_962884_opt_in_disable_hyphens.js] +[browser_963639_customizing_attribute_non_customizable_toolbar.js] +[browser_967000_button_charEncoding.js] +[browser_967000_button_feeds.js] +[browser_967000_button_sync.js] +[browser_968447_bookmarks_toolbar_items_in_panel.js] +skip-if = os == "linux" # Intemittent failures - bug 979207 +[browser_968565_insert_before_hidden_items.js] +[browser_969427_recreate_destroyed_widget_after_reset.js] +[browser_969661_character_encoding_navbar_disabled.js] +[browser_970511_undo_restore_default.js] +[browser_972267_customizationchange_events.js] +[browser_973641_button_addon.js] +[browser_973932_addonbar_currentset.js] +[browser_975719_customtoolbars_behaviour.js] +[browser_976792_insertNodeInWindow.js] +skip-if = os == "linux" +[browser_978084_dragEnd_after_move.js] +[browser_980155_add_overflow_toolbar.js] +[browser_981305_separator_insertion.js] +[browser_981418-widget-onbeforecreated-handler.js] +[browser_982656_restore_defaults_builtin_widgets.js] +[browser_984455_bookmarks_items_reparenting.js] +skip-if = os == "linux" +[browser_985815_propagate_setToolbarVisibility.js] +[browser_987177_destroyWidget_xul.js] +[browser_987177_xul_wrapper_updating.js] +[browser_987185_syncButton.js] +[browser_987492_window_api.js] +[browser_987640_charEncoding.js] +[browser_988072_sidebar_events.js] +[browser_989338_saved_placements_not_resaved.js] +[browser_989751_subviewbutton_class.js] +[browser_992747_toggle_noncustomizable_toolbar.js] +[browser_993322_widget_notoolbar.js] +[browser_995164_registerArea_during_customize_mode.js] +[browser_996364_registerArea_different_properties.js] +[browser_996635_remove_non_widgets.js] +[browser_1003588_no_specials_in_panel.js] +[browser_1007336_lwthemes_in_customize_mode.js] +skip-if = os == "linux" # crashing on Linux due to bug 1271683 +[browser_1008559_anchor_undo_restore.js] +[browser_1042100_default_placements_update.js] +[browser_1058573_showToolbarsDropdown.js] +[browser_1087303_button_fullscreen.js] +tags = fullscreen +skip-if = os == "mac" +[browser_1087303_button_preferences.js] +[browser_1089591_still_customizable_after_reset.js] +[browser_1096763_seen_widgets_post_reset.js] +[browser_1161838_inserted_new_default_buttons.js] +[browser_bootstrapped_custom_toolbar.js] +[browser_customizemode_contextmenu_menubuttonstate.js] +[browser_panel_toggle.js] +[browser_switch_to_customize_mode.js] +[browser_check_tooltips_in_navbar.js] diff --git a/browser/components/customizableui/test/browser_1003588_no_specials_in_panel.js b/browser/components/customizableui/test/browser_1003588_no_specials_in_panel.js new file mode 100644 index 000000000..22fbb5c0c --- /dev/null +++ b/browser/components/customizableui/test/browser_1003588_no_specials_in_panel.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +function simulateItemDragAndEnd(aToDrag, aTarget) { + var ds = Components.classes["@mozilla.org/widget/dragservice;1"]. + getService(Components.interfaces.nsIDragService); + + ds.startDragSession(); + try { + var [result, dataTransfer] = EventUtils.synthesizeDragOver(aToDrag.parentNode, aTarget); + EventUtils.synthesizeDropAfterDragOver(result, dataTransfer, aTarget); + // Send dragend to move dragging item back to initial place. + EventUtils.sendDragEvent({ type: "dragend", dataTransfer: dataTransfer }, + aToDrag.parentNode); + } finally { + ds.endDragSession(true); + } +} + +add_task(function* checkNoAddingToPanel() { + let area = CustomizableUI.AREA_PANEL; + let previousPlacements = getAreaWidgetIds(area); + CustomizableUI.addWidgetToArea("separator", area); + CustomizableUI.addWidgetToArea("spring", area); + CustomizableUI.addWidgetToArea("spacer", area); + assertAreaPlacements(area, previousPlacements); + + let oldNumberOfItems = previousPlacements.length; + if (getAreaWidgetIds(area).length != oldNumberOfItems) { + CustomizableUI.reset(); + } +}); + +add_task(function* checkAddingToToolbar() { + let area = CustomizableUI.AREA_NAVBAR; + let previousPlacements = getAreaWidgetIds(area); + CustomizableUI.addWidgetToArea("separator", area); + CustomizableUI.addWidgetToArea("spring", area); + CustomizableUI.addWidgetToArea("spacer", area); + let expectedPlacements = [...previousPlacements].concat([ + /separator/, + /spring/, + /spacer/ + ]); + assertAreaPlacements(area, expectedPlacements); + + let newlyAddedElements = getAreaWidgetIds(area).slice(-3); + while (newlyAddedElements.length) { + CustomizableUI.removeWidgetFromArea(newlyAddedElements.shift()); + } + + assertAreaPlacements(area, previousPlacements); + + let oldNumberOfItems = previousPlacements.length; + if (getAreaWidgetIds(area).length != oldNumberOfItems) { + CustomizableUI.reset(); + } +}); + + +add_task(function* checkDragging() { + let startArea = CustomizableUI.AREA_NAVBAR; + let targetArea = CustomizableUI.AREA_PANEL; + let startingToolbarPlacements = getAreaWidgetIds(startArea); + let startingTargetPlacements = getAreaWidgetIds(targetArea); + + CustomizableUI.addWidgetToArea("separator", startArea); + CustomizableUI.addWidgetToArea("spring", startArea); + CustomizableUI.addWidgetToArea("spacer", startArea); + + let placementsWithSpecials = getAreaWidgetIds(startArea); + let elementsToMove = []; + for (let id of placementsWithSpecials) { + if (CustomizableUI.isSpecialWidget(id)) { + elementsToMove.push(id); + } + } + is(elementsToMove.length, 3, "Should have 3 elements to try and drag."); + + yield startCustomizing(); + for (let id of elementsToMove) { + simulateItemDragAndEnd(document.getElementById(id), PanelUI.contents); + } + + assertAreaPlacements(startArea, placementsWithSpecials); + assertAreaPlacements(targetArea, startingTargetPlacements); + + for (let id of elementsToMove) { + simulateItemDrag(document.getElementById(id), gCustomizeMode.visiblePalette); + } + + assertAreaPlacements(startArea, startingToolbarPlacements); + assertAreaPlacements(targetArea, startingTargetPlacements); + + ok(!gCustomizeMode.visiblePalette.querySelector("toolbarspring,toolbarseparator,toolbarspacer"), + "No specials should make it to the palette alive."); + yield endCustomizing(); +}); + + +add_task(function* asyncCleanup() { + yield endCustomizing(); + CustomizableUI.reset(); +}); + diff --git a/browser/components/customizableui/test/browser_1007336_lwthemes_in_customize_mode.js b/browser/components/customizableui/test/browser_1007336_lwthemes_in_customize_mode.js new file mode 100644 index 000000000..db4f88e6d --- /dev/null +++ b/browser/components/customizableui/test/browser_1007336_lwthemes_in_customize_mode.js @@ -0,0 +1,108 @@ +/* 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 DEFAULT_THEME_ID = "{972ce4c6-7e08-4474-a285-3208198ce6fd}"; +const {LightweightThemeManager} = Components.utils.import("resource://gre/modules/LightweightThemeManager.jsm", {}); + +add_task(function* () { + Services.prefs.clearUserPref("lightweightThemes.usedThemes"); + Services.prefs.clearUserPref("lightweightThemes.recommendedThemes"); + LightweightThemeManager.clearBuiltInThemes(); + + yield startCustomizing(); + + let themesButton = document.getElementById("customization-lwtheme-button"); + let popup = document.getElementById("customization-lwtheme-menu"); + + let popupShownPromise = popupShown(popup); + EventUtils.synthesizeMouseAtCenter(themesButton, {}); + info("Clicked on themes button"); + yield popupShownPromise; + + // close current tab and re-open Customize menu to confirm correct number of Themes + yield endCustomizing(); + info("Exited customize mode"); + yield startCustomizing(); + info("Started customizing a second time"); + popupShownPromise = popupShown(popup); + EventUtils.synthesizeMouseAtCenter(themesButton, {}); + info("Clicked on themes button a second time"); + yield popupShownPromise; + + let header = document.getElementById("customization-lwtheme-menu-header"); + let recommendedHeader = document.getElementById("customization-lwtheme-menu-recommended"); + + is(header.nextSibling.nextSibling, recommendedHeader, + "There should only be one theme (default) in the 'My Themes' section by default"); + is(header.nextSibling.theme.id, DEFAULT_THEME_ID, "That theme should be the default theme"); + + let firstLWTheme = recommendedHeader.nextSibling; + let firstLWThemeId = firstLWTheme.theme.id; + let themeChangedPromise = promiseObserverNotified("lightweight-theme-changed"); + firstLWTheme.doCommand(); + info("Clicked on first theme"); + yield themeChangedPromise; + + popupShownPromise = popupShown(popup); + EventUtils.synthesizeMouseAtCenter(themesButton, {}); + info("Clicked on themes button"); + yield popupShownPromise; + + is(header.nextSibling.theme.id, DEFAULT_THEME_ID, "The first theme should be the Default theme"); + let installedThemeId = header.nextSibling.nextSibling.theme.id; + ok(installedThemeId.startsWith(firstLWThemeId), + "The second theme in the 'My Themes' section should be the newly installed theme: " + + "Installed theme id: " + installedThemeId + "; First theme ID: " + firstLWThemeId); + is(header.nextSibling.nextSibling.nextSibling, recommendedHeader, + "There should be two themes in the 'My Themes' section"); + + let defaultTheme = header.nextSibling; + defaultTheme.doCommand(); + is(Services.prefs.getCharPref("lightweightThemes.selectedThemeID"), "", "No lwtheme should be selected"); + + // ensure current theme isn't set to "Default" + popupShownPromise = popupShown(popup); + EventUtils.synthesizeMouseAtCenter(themesButton, {}); + info("Clicked on themes button a second time"); + yield popupShownPromise; + + firstLWTheme = recommendedHeader.nextSibling; + themeChangedPromise = promiseObserverNotified("lightweight-theme-changed"); + firstLWTheme.doCommand(); + info("Clicked on first theme again"); + yield themeChangedPromise; + + // check that "Restore Defaults" button resets theme + yield gCustomizeMode.reset(); + is(LightweightThemeManager.currentTheme, null, "Current theme reset to default"); + + yield endCustomizing(); + Services.prefs.setCharPref("lightweightThemes.usedThemes", "[]"); + Services.prefs.setCharPref("lightweightThemes.recommendedThemes", "[]"); + info("Removed all recommended themes"); + yield startCustomizing(); + popupShownPromise = popupShown(popup); + EventUtils.synthesizeMouseAtCenter(themesButton, {}); + info("Clicked on themes button a second time"); + yield popupShownPromise; + header = document.getElementById("customization-lwtheme-menu-header"); + is(header.hidden, false, "Header should never be hidden"); + is(header.nextSibling.theme.id, DEFAULT_THEME_ID, "The first theme should be the Default theme"); + is(header.nextSibling.hidden, false, "The default theme should never be hidden"); + recommendedHeader = document.getElementById("customization-lwtheme-menu-recommended"); + is(header.nextSibling.nextSibling, recommendedHeader, + "There should only be one theme (default) in the 'My Themes' section by default"); + let footer = document.getElementById("customization-lwtheme-menu-footer"); + is(recommendedHeader.nextSibling.id, footer.id, "There should be no recommended themes in the menu"); + is(recommendedHeader.hidden, true, "The recommendedHeader should be hidden since there are no recommended themes"); +}); + +add_task(function* asyncCleanup() { + yield endCustomizing(); + + Services.prefs.clearUserPref("lightweightThemes.usedThemes"); + Services.prefs.clearUserPref("lightweightThemes.recommendedThemes"); +}); diff --git a/browser/components/customizableui/test/browser_1008559_anchor_undo_restore.js b/browser/components/customizableui/test/browser_1008559_anchor_undo_restore.js new file mode 100644 index 000000000..56657914b --- /dev/null +++ b/browser/components/customizableui/test/browser_1008559_anchor_undo_restore.js @@ -0,0 +1,71 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +const kAnchorAttribute = "cui-anchorid"; + +/** + * Check that anchor gets set correctly when moving an item from the panel to the toolbar + * using 'undo' + */ +add_task(function*() { + yield startCustomizing(); + let button = document.getElementById("history-panelmenu"); + is(button.getAttribute(kAnchorAttribute), "PanelUI-menu-button", + "Button (" + button.id + ") starts out with correct anchor"); + + let navbar = document.getElementById("nav-bar").customizationTarget; + simulateItemDrag(button, navbar); + is(CustomizableUI.getPlacementOfWidget(button.id).area, "nav-bar", + "Button (" + button.id + ") ends up in nav-bar"); + + ok(!button.hasAttribute(kAnchorAttribute), + "Button (" + button.id + ") has no anchor in toolbar"); + + let resetButton = document.getElementById("customization-reset-button"); + ok(!resetButton.hasAttribute("disabled"), "Should be able to reset now."); + yield gCustomizeMode.reset(); + + is(button.getAttribute(kAnchorAttribute), "PanelUI-menu-button", + "Button (" + button.id + ") has anchor again"); + + let undoButton = document.getElementById("customization-undo-reset-button"); + ok(!undoButton.hasAttribute("disabled"), "Should be able to undo now."); + yield gCustomizeMode.undoReset(); + + ok(!button.hasAttribute(kAnchorAttribute), + "Button (" + button.id + ") once again has no anchor in toolbar"); + + yield gCustomizeMode.reset(); + + yield endCustomizing(); +}); + + +/** + * Check that anchor gets set correctly when moving an item from the panel to the toolbar + * using 'reset' + */ +add_task(function*() { + yield startCustomizing(); + let button = document.getElementById("bookmarks-menu-button"); + ok(!button.hasAttribute(kAnchorAttribute), + "Button (" + button.id + ") has no anchor in toolbar"); + + let panel = document.getElementById("PanelUI-contents"); + simulateItemDrag(button, panel); + is(CustomizableUI.getPlacementOfWidget(button.id).area, "PanelUI-contents", + "Button (" + button.id + ") ends up in panel"); + is(button.getAttribute(kAnchorAttribute), "PanelUI-menu-button", + "Button (" + button.id + ") has correct anchor in the panel"); + + let resetButton = document.getElementById("customization-reset-button"); + ok(!resetButton.hasAttribute("disabled"), "Should be able to reset now."); + yield gCustomizeMode.reset(); + + ok(!button.hasAttribute(kAnchorAttribute), + "Button (" + button.id + ") once again has no anchor in toolbar"); + + yield endCustomizing(); +}); diff --git a/browser/components/customizableui/test/browser_1042100_default_placements_update.js b/browser/components/customizableui/test/browser_1042100_default_placements_update.js new file mode 100644 index 000000000..129dbd754 --- /dev/null +++ b/browser/components/customizableui/test/browser_1042100_default_placements_update.js @@ -0,0 +1,107 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// NB: This uses some ugly hacks to get into the CUI module from elsewhere... +// don't try this at home, kids. +function test() { + // Customize something to make sure stuff changed: + CustomizableUI.addWidgetToArea("feed-button", CustomizableUI.AREA_NAVBAR); + + // Check what version we're on: + let CustomizableUIBSPass = Cu.import("resource:///modules/CustomizableUI.jsm", {}); + + is(CustomizableUIBSPass.gFuturePlacements.size, 0, + "All future placements should be dealt with by now."); + + let {CustomizableUIInternal, gFuturePlacements, gPalette} = CustomizableUIBSPass; + CustomizableUIInternal._introduceNewBuiltinWidgets(); + is(gFuturePlacements.size, 0, + "No change to future placements initially."); + + let currentVersion = CustomizableUIBSPass.kVersion; + + + // Add our widget to the defaults: + let testWidgetNew = { + id: "test-messing-with-default-placements-new", + label: "Test messing with default placements - should be inserted", + defaultArea: CustomizableUI.AREA_NAVBAR, + introducedInVersion: currentVersion + 1, + }; + + let normalizedWidget = CustomizableUIInternal.normalizeWidget(testWidgetNew, + CustomizableUI.SOURCE_BUILTIN); + ok(normalizedWidget, "Widget should be normalizable"); + if (!normalizedWidget) { + return; + } + CustomizableUIBSPass.gPalette.set(testWidgetNew.id, normalizedWidget); + + let testWidgetOld = { + id: "test-messing-with-default-placements-old", + label: "Test messing with default placements - should NOT be inserted", + defaultArea: CustomizableUI.AREA_NAVBAR, + introducedInVersion: currentVersion, + }; + + normalizedWidget = CustomizableUIInternal.normalizeWidget(testWidgetOld, + CustomizableUI.SOURCE_BUILTIN); + ok(normalizedWidget, "Widget should be normalizable"); + if (!normalizedWidget) { + return; + } + CustomizableUIBSPass.gPalette.set(testWidgetOld.id, normalizedWidget); + + + // Now increase the version in the module: + CustomizableUIBSPass.kVersion++; + + let hadSavedState = !!CustomizableUIBSPass.gSavedState + if (!hadSavedState) { + CustomizableUIBSPass.gSavedState = {currentVersion: CustomizableUIBSPass.kVersion - 1}; + } + + // Then call the re-init routine so we re-add the builtin widgets + CustomizableUIInternal._introduceNewBuiltinWidgets(); + is(gFuturePlacements.size, 1, + "Should have 1 more future placement"); + let haveNavbarPlacements = gFuturePlacements.has(CustomizableUI.AREA_NAVBAR); + ok(haveNavbarPlacements, "Should have placements for nav-bar"); + if (haveNavbarPlacements) { + let placements = [...gFuturePlacements.get(CustomizableUI.AREA_NAVBAR)]; + + // Ignore widgets that are placed using the pref facility and not the + // versioned facility. They're independent of kVersion and the saved + // state's current version, so they may be present in the placements. + for (let i = 0; i < placements.length; ) { + if (placements[i] == testWidgetNew.id) { + i++; + continue; + } + let pref = "browser.toolbarbuttons.introduced." + placements[i]; + let introduced = false; + try { + introduced = Services.prefs.getBoolPref(pref); + } catch (ex) {} + if (!introduced) { + i++; + continue; + } + placements.splice(i, 1); + } + + is(placements.length, 1, "Should have 1 newly placed widget in nav-bar"); + is(placements[0], testWidgetNew.id, "Should have our test widget to be placed in nav-bar"); + } + + gFuturePlacements.delete(CustomizableUI.AREA_NAVBAR); + CustomizableUIBSPass.kVersion--; + gPalette.delete(testWidgetNew.id); + gPalette.delete(testWidgetOld.id); + if (!hadSavedState) { + CustomizableUIBSPass.gSavedState = null; + } +} + diff --git a/browser/components/customizableui/test/browser_1058573_showToolbarsDropdown.js b/browser/components/customizableui/test/browser_1058573_showToolbarsDropdown.js new file mode 100644 index 000000000..42a032ff8 --- /dev/null +++ b/browser/components/customizableui/test/browser_1058573_showToolbarsDropdown.js @@ -0,0 +1,25 @@ +/* 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"; + +add_task(function*() { + info("Check that toggleable toolbars dropdown in always shown"); + + info("Remove all possible custom toolbars"); + yield removeCustomToolbars(); + + info("Enter customization mode"); + yield startCustomizing(); + + let toolbarsToggle = document.getElementById("customization-toolbar-visibility-button"); + ok(toolbarsToggle, "The toolbars toggle dropdown exists"); + ok(!toolbarsToggle.hasAttribute("hidden"), + "The toolbars toggle dropdown is displayed"); +}); + +add_task(function* asyncCleanup() { + info("Exit customization mode"); + yield endCustomizing(); +}); diff --git a/browser/components/customizableui/test/browser_1087303_button_fullscreen.js b/browser/components/customizableui/test/browser_1087303_button_fullscreen.js new file mode 100644 index 000000000..c6b87d6ab --- /dev/null +++ b/browser/components/customizableui/test/browser_1087303_button_fullscreen.js @@ -0,0 +1,46 @@ +/* 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"; + +add_task(function*() { + info("Check fullscreen button existence and functionality"); + + yield PanelUI.show(); + + let fullscreenButton = document.getElementById("fullscreen-button"); + ok(fullscreenButton, "Fullscreen button appears in Panel Menu"); + + let fullscreenPromise = promiseFullscreenChange(); + fullscreenButton.click(); + yield fullscreenPromise; + + ok(window.fullScreen, "Fullscreen mode was opened"); + + // exit full screen mode + fullscreenPromise = promiseFullscreenChange(); + window.fullScreen = !window.fullScreen; + yield fullscreenPromise; + + ok(!window.fullScreen, "Successfully exited fullscreen"); +}); + +function promiseFullscreenChange() { + let deferred = Promise.defer(); + info("Wait for fullscreen change"); + + let timeoutId = setTimeout(() => { + window.removeEventListener("fullscreen", onFullscreenChange, true); + deferred.reject("Fullscreen change did not happen within " + 20000 + "ms"); + }, 20000); + + function onFullscreenChange(event) { + clearTimeout(timeoutId); + window.removeEventListener("fullscreen", onFullscreenChange, true); + info("Fullscreen event received"); + deferred.resolve(); + } + window.addEventListener("fullscreen", onFullscreenChange, true); + return deferred.promise; +} diff --git a/browser/components/customizableui/test/browser_1087303_button_preferences.js b/browser/components/customizableui/test/browser_1087303_button_preferences.js new file mode 100644 index 000000000..b1fdb85b6 --- /dev/null +++ b/browser/components/customizableui/test/browser_1087303_button_preferences.js @@ -0,0 +1,50 @@ +/* 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"; + +var newTab = null; + +add_task(function*() { + info("Check preferences button existence and functionality"); + + yield PanelUI.show(); + info("Menu panel was opened"); + + let preferencesButton = document.getElementById("preferences-button"); + ok(preferencesButton, "Preferences button exists in Panel Menu"); + preferencesButton.click(); + + newTab = gBrowser.selectedTab; + yield waitForPageLoad(newTab); + + let openedPage = gBrowser.currentURI.spec; + is(openedPage, "about:preferences", "Preferences page was opened"); +}); + +add_task(function asyncCleanup() { + if (gBrowser.tabs.length == 1) + gBrowser.addTab("about:blank"); + + gBrowser.removeTab(gBrowser.selectedTab); + info("Tabs were restored"); +}); + +function waitForPageLoad(aTab) { + let deferred = Promise.defer(); + + let timeoutId = setTimeout(() => { + aTab.linkedBrowser.removeEventListener("load", onTabLoad, true); + deferred.reject("Page didn't load within " + 20000 + "ms"); + }, 20000); + + function onTabLoad(event) { + clearTimeout(timeoutId); + aTab.linkedBrowser.removeEventListener("load", onTabLoad, true); + info("Tab event received: " + "load"); + deferred.resolve(); + } + aTab.linkedBrowser.addEventListener("load", onTabLoad, true, true); + return deferred.promise; +} diff --git a/browser/components/customizableui/test/browser_1089591_still_customizable_after_reset.js b/browser/components/customizableui/test/browser_1089591_still_customizable_after_reset.js new file mode 100644 index 000000000..1f502e8e2 --- /dev/null +++ b/browser/components/customizableui/test/browser_1089591_still_customizable_after_reset.js @@ -0,0 +1,24 @@ +"use strict"; + +// Dragging the elements again after a reset should work +add_task(function* () { + yield startCustomizing(); + let historyButton = document.getElementById("wrapper-history-panelmenu"); + let devButton = document.getElementById("wrapper-developer-button"); + + ok(historyButton && devButton, "Draggable elements should exist"); + simulateItemDrag(historyButton, devButton); + yield gCustomizeMode.reset(); + ok(CustomizableUI.inDefaultState, "Should be back in default state"); + + historyButton = document.getElementById("wrapper-history-panelmenu"); + devButton = document.getElementById("wrapper-developer-button"); + ok(historyButton && devButton, "Draggable elements should exist"); + simulateItemDrag(historyButton, devButton); + + yield endCustomizing(); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_1096763_seen_widgets_post_reset.js b/browser/components/customizableui/test/browser_1096763_seen_widgets_post_reset.js new file mode 100644 index 000000000..b5a325afb --- /dev/null +++ b/browser/components/customizableui/test/browser_1096763_seen_widgets_post_reset.js @@ -0,0 +1,31 @@ +"use strict"; + +const BUTTONID = "test-seenwidget-post-reset"; + +add_task(function*() { + CustomizableUI.createWidget({ + id: BUTTONID, + label: "Test widget seen post reset", + defaultArea: CustomizableUI.AREA_NAVBAR + }); + + const kPrefCustomizationState = "browser.uiCustomization.state"; + let bsPass = Cu.import("resource:///modules/CustomizableUI.jsm", {}); + ok(bsPass.gSeenWidgets.has(BUTTONID), "Widget should be seen after createWidget is called."); + CustomizableUI.reset(); + ok(bsPass.gSeenWidgets.has(BUTTONID), "Widget should still be seen after reset."); + ok(!Services.prefs.prefHasUserValue(kPrefCustomizationState), "Pref shouldn't be set right now, because that'd break undo."); + CustomizableUI.addWidgetToArea(BUTTONID, CustomizableUI.AREA_NAVBAR); + gCustomizeMode.removeFromArea(document.getElementById(BUTTONID)); + let hasUserValue = Services.prefs.prefHasUserValue(kPrefCustomizationState); + ok(hasUserValue, "Pref should be set right now."); + if (hasUserValue) { + let seenArray = JSON.parse(Services.prefs.getCharPref(kPrefCustomizationState)).seen; + isnot(seenArray.indexOf(BUTTONID), -1, "Widget should be in saved 'seen' list."); + } +}); + +registerCleanupFunction(function() { + CustomizableUI.destroyWidget(BUTTONID); + CustomizableUI.reset(); +}); diff --git a/browser/components/customizableui/test/browser_1161838_inserted_new_default_buttons.js b/browser/components/customizableui/test/browser_1161838_inserted_new_default_buttons.js new file mode 100644 index 000000000..42768debf --- /dev/null +++ b/browser/components/customizableui/test/browser_1161838_inserted_new_default_buttons.js @@ -0,0 +1,78 @@ +"use strict"; + +// NB: This uses some ugly hacks to get into the CUI module from elsewhere... +// don't try this at home, kids. +function test() { + // Customize something to make sure stuff changed: + CustomizableUI.addWidgetToArea("feed-button", CustomizableUI.AREA_NAVBAR); + + let CustomizableUIBSPass = Cu.import("resource:///modules/CustomizableUI.jsm", {}); + + is(CustomizableUIBSPass.gFuturePlacements.size, 0, + "All future placements should be dealt with by now."); + + let {CustomizableUIInternal, gFuturePlacements, gPalette} = CustomizableUIBSPass; + + // Force us to have a saved state: + CustomizableUIInternal.saveState(); + CustomizableUIInternal.loadSavedState(); + + CustomizableUIInternal._introduceNewBuiltinWidgets(); + is(gFuturePlacements.size, 0, + "No change to future placements initially."); + + // Add our widget to the defaults: + let testWidgetNew = { + id: "test-messing-with-default-placements-new-pref", + label: "Test messing with default placements - pref-based", + defaultArea: CustomizableUI.AREA_NAVBAR, + introducedInVersion: "pref", + }; + + let normalizedWidget = CustomizableUIInternal.normalizeWidget(testWidgetNew, + CustomizableUI.SOURCE_BUILTIN); + ok(normalizedWidget, "Widget should be normalizable"); + if (!normalizedWidget) { + return; + } + CustomizableUIBSPass.gPalette.set(testWidgetNew.id, normalizedWidget); + + // Now adjust default placements for area: + let navbarArea = CustomizableUIBSPass.gAreas.get(CustomizableUI.AREA_NAVBAR); + let navbarPlacements = navbarArea.get("defaultPlacements"); + navbarPlacements.splice(navbarPlacements.indexOf("bookmarks-menu-button") + 1, 0, testWidgetNew.id); + + let savedPlacements = CustomizableUIBSPass.gSavedState.placements[CustomizableUI.AREA_NAVBAR]; + // Then call the re-init routine so we re-add the builtin widgets + CustomizableUIInternal._introduceNewBuiltinWidgets(); + is(gFuturePlacements.size, 1, + "Should have 1 more future placement"); + let futureNavbarPlacements = gFuturePlacements.get(CustomizableUI.AREA_NAVBAR); + ok(futureNavbarPlacements, "Should have placements for nav-bar"); + if (futureNavbarPlacements) { + ok(futureNavbarPlacements.has(testWidgetNew.id), "widget should be in future placements"); + } + CustomizableUIInternal._placeNewDefaultWidgetsInArea(CustomizableUI.AREA_NAVBAR); + + let indexInSavedPlacements = savedPlacements.indexOf(testWidgetNew.id); + info("Saved placements: " + savedPlacements.join(', ')); + isnot(indexInSavedPlacements, -1, "Widget should have been inserted"); + is(indexInSavedPlacements, savedPlacements.indexOf("bookmarks-menu-button") + 1, + "Widget should be in the right place."); + + if (futureNavbarPlacements) { + ok(!futureNavbarPlacements.has(testWidgetNew.id), "widget should be out of future placements"); + } + + if (indexInSavedPlacements != -1) { + savedPlacements.splice(indexInSavedPlacements, 1); + } + + gFuturePlacements.delete(CustomizableUI.AREA_NAVBAR); + let indexInDefaultPlacements = navbarPlacements.indexOf(testWidgetNew.id); + if (indexInDefaultPlacements != -1) { + navbarPlacements.splice(indexInDefaultPlacements, 1); + } + gPalette.delete(testWidgetNew.id); + CustomizableUI.reset(); +} diff --git a/browser/components/customizableui/test/browser_873501_handle_specials.js b/browser/components/customizableui/test/browser_873501_handle_specials.js new file mode 100644 index 000000000..b07c8e0d7 --- /dev/null +++ b/browser/components/customizableui/test/browser_873501_handle_specials.js @@ -0,0 +1,79 @@ +/* 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 kToolbarName = "test-specials-toolbar"; + +registerCleanupFunction(removeCustomToolbars); + +// Add a toolbar with two springs and the downloads button. +add_task(function* addToolbarWith2SpringsAndDownloadsButton() { + // Create the toolbar with a single spring: + createToolbarWithPlacements(kToolbarName, ["spring"]); + ok(document.getElementById(kToolbarName), "Toolbar should be created."); + + // Check it's there with a generated ID: + assertAreaPlacements(kToolbarName, [/customizableui-special-spring\d+/]); + let [springId] = getAreaWidgetIds(kToolbarName); + + // Add a second spring, check if that's there and doesn't share IDs + CustomizableUI.addWidgetToArea("spring", kToolbarName); + assertAreaPlacements(kToolbarName, [springId, + /customizableui-special-spring\d+/]); + let [, spring2Id] = getAreaWidgetIds(kToolbarName); + + isnot(springId, spring2Id, "Springs shouldn't have identical IDs."); + + // Try moving the downloads button to this new toolbar, between the two springs: + CustomizableUI.addWidgetToArea("downloads-button", kToolbarName, 1); + assertAreaPlacements(kToolbarName, [springId, "downloads-button", spring2Id]); + yield removeCustomToolbars(); +}); + +// Add separators around the downloads button. +add_task(function* addSeparatorsAroundDownloadsButton() { + createToolbarWithPlacements(kToolbarName, ["separator"]); + ok(document.getElementById(kToolbarName), "Toolbar should be created."); + + // Check it's there with a generated ID: + assertAreaPlacements(kToolbarName, [/customizableui-special-separator\d+/]); + let [separatorId] = getAreaWidgetIds(kToolbarName); + + CustomizableUI.addWidgetToArea("separator", kToolbarName); + assertAreaPlacements(kToolbarName, [separatorId, + /customizableui-special-separator\d+/]); + let [, separator2Id] = getAreaWidgetIds(kToolbarName); + + isnot(separatorId, separator2Id, "Separator ids shouldn't be equal."); + + CustomizableUI.addWidgetToArea("downloads-button", kToolbarName, 1); + assertAreaPlacements(kToolbarName, [separatorId, "downloads-button", separator2Id]); + yield removeCustomToolbars(); +}); + +// Add spacers around the downloads button. +add_task(function* addSpacersAroundDownloadsButton() { + createToolbarWithPlacements(kToolbarName, ["spacer"]); + ok(document.getElementById(kToolbarName), "Toolbar should be created."); + + // Check it's there with a generated ID: + assertAreaPlacements(kToolbarName, [/customizableui-special-spacer\d+/]); + let [spacerId] = getAreaWidgetIds(kToolbarName); + + CustomizableUI.addWidgetToArea("spacer", kToolbarName); + assertAreaPlacements(kToolbarName, [spacerId, + /customizableui-special-spacer\d+/]); + let [, spacer2Id] = getAreaWidgetIds(kToolbarName); + + isnot(spacerId, spacer2Id, "Spacer ids shouldn't be equal."); + + CustomizableUI.addWidgetToArea("downloads-button", kToolbarName, 1); + assertAreaPlacements(kToolbarName, [spacerId, "downloads-button", spacer2Id]); + yield removeCustomToolbars(); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_876926_customize_mode_wrapping.js b/browser/components/customizableui/test/browser_876926_customize_mode_wrapping.js new file mode 100644 index 000000000..a3204c271 --- /dev/null +++ b/browser/components/customizableui/test/browser_876926_customize_mode_wrapping.js @@ -0,0 +1,185 @@ +/* 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 kXULWidgetId = "a-test-button"; // we'll create a button with this ID. +const kAPIWidgetId = "feed-button"; +const kPanel = CustomizableUI.AREA_PANEL; +const kToolbar = CustomizableUI.AREA_NAVBAR; +const kVisiblePalette = "customization-palette"; +const kPlaceholderClass = "panel-customization-placeholder"; + +function checkWrapper(id) { + is(document.querySelectorAll("#wrapper-" + id).length, 1, "There should be exactly 1 wrapper for " + id + " in the customizing window."); +} + +var move = { + "drag": function(id, target) { + let targetNode = document.getElementById(target); + if (targetNode.customizationTarget) { + targetNode = targetNode.customizationTarget; + } + simulateItemDrag(document.getElementById(id), targetNode); + }, + "dragToItem": function(id, target) { + let targetNode = document.getElementById(target); + if (targetNode.customizationTarget) { + targetNode = targetNode.customizationTarget; + } + let items = targetNode.querySelectorAll("toolbarpaletteitem:not(." + kPlaceholderClass + ")"); + if (target == kPanel) { + targetNode = items[items.length - 1]; + } else { + targetNode = items[0]; + } + simulateItemDrag(document.getElementById(id), targetNode); + }, + "API": function(id, target) { + if (target == kVisiblePalette) { + return CustomizableUI.removeWidgetFromArea(id); + } + return CustomizableUI.addWidgetToArea(id, target, null); + } +}; + +function isLast(containerId, defaultPlacements, id) { + assertAreaPlacements(containerId, defaultPlacements.concat([id])); + is(document.getElementById(containerId).customizationTarget.lastChild.firstChild.id, id, + "Widget " + id + " should be in " + containerId + " in customizing window."); + is(otherWin.document.getElementById(containerId).customizationTarget.lastChild.id, id, + "Widget " + id + " should be in " + containerId + " in other window."); +} + +function getLastVisibleNodeInToolbar(containerId, win=window) { + let container = win.document.getElementById(containerId).customizationTarget; + let rv = container.lastChild; + while (rv && (rv.getAttribute('hidden') == 'true' || (rv.firstChild && rv.firstChild.getAttribute('hidden') == 'true'))) { + rv = rv.previousSibling; + } + return rv; +} + +function isLastVisibleInToolbar(containerId, defaultPlacements, id) { + let newPlacements; + for (let i = defaultPlacements.length - 1; i >= 0; i--) { + let el = document.getElementById(defaultPlacements[i]); + if (el && el.getAttribute('hidden') != 'true') { + newPlacements = [...defaultPlacements]; + newPlacements.splice(i + 1, 0, id); + break; + } + } + if (!newPlacements) { + assertAreaPlacements(containerId, defaultPlacements.concat([id])); + } else { + assertAreaPlacements(containerId, newPlacements); + } + is(getLastVisibleNodeInToolbar(containerId).firstChild.id, id, + "Widget " + id + " should be in " + containerId + " in customizing window."); + is(getLastVisibleNodeInToolbar(containerId, otherWin).id, id, + "Widget " + id + " should be in " + containerId + " in other window."); +} + +function isFirst(containerId, defaultPlacements, id) { + assertAreaPlacements(containerId, [id].concat(defaultPlacements)); + is(document.getElementById(containerId).customizationTarget.firstChild.firstChild.id, id, + "Widget " + id + " should be in " + containerId + " in customizing window."); + is(otherWin.document.getElementById(containerId).customizationTarget.firstChild.id, id, + "Widget " + id + " should be in " + containerId + " in other window."); +} + +function checkToolbar(id, method) { + // Place at start of the toolbar: + let toolbarPlacements = getAreaWidgetIds(kToolbar); + move[method](id, kToolbar); + if (method == "dragToItem") { + isFirst(kToolbar, toolbarPlacements, id); + } else if (method == "drag") { + isLastVisibleInToolbar(kToolbar, toolbarPlacements, id); + } else { + isLast(kToolbar, toolbarPlacements, id); + } + checkWrapper(id); +} + +function checkPanel(id, method) { + let panelPlacements = getAreaWidgetIds(kPanel); + move[method](id, kPanel); + let children = document.getElementById(kPanel).querySelectorAll("toolbarpaletteitem:not(." + kPlaceholderClass + ")"); + let otherChildren = otherWin.document.getElementById(kPanel).children; + let newPlacements = panelPlacements.concat([id]); + // Relative position of the new item from the end: + let position = -1; + // For the drag to item case, we drag to the last item, making the dragged item the + // penultimate item. We can't well use the first item because the panel has complicated + // rules about rearranging wide items (which, by default, the first two items are). + if (method == "dragToItem") { + newPlacements.pop(); + newPlacements.splice(panelPlacements.length - 1, 0, id); + position = -2; + } + assertAreaPlacements(kPanel, newPlacements); + is(children[children.length + position].firstChild.id, id, + "Widget " + id + " should be in " + kPanel + " in customizing window."); + is(otherChildren[otherChildren.length + position].id, id, + "Widget " + id + " should be in " + kPanel + " in other window."); + checkWrapper(id); +} + +function checkPalette(id, method) { + // Move back to palette: + move[method](id, kVisiblePalette); + ok(CustomizableUI.inDefaultState, "Should end in default state"); + let visibleChildren = gCustomizeMode.visiblePalette.children; + let expectedChild = method == "dragToItem" ? visibleChildren[0] : visibleChildren[visibleChildren.length - 1]; + is(expectedChild.firstChild.id, id, "Widget " + id + " was moved using " + method + " and should now be wrapped in palette in customizing window."); + if (id == kXULWidgetId) { + ok(otherWin.gNavToolbox.palette.querySelector("#" + id), "Widget " + id + " should be in invisible palette in other window."); + } + checkWrapper(id); +} + +// This test needs a XUL button that's in the palette by default. No such +// button currently exists, so we create a simple one. +function createXULButtonForWindow(win) { + createDummyXULButton(kXULWidgetId, "test-button", win); +} + +function removeXULButtonForWindow(win) { + win.gNavToolbox.palette.querySelector(`#${kXULWidgetId}`).remove(); +} + +var otherWin; + +// Moving widgets in two windows, one with customize mode and one without, should work. +add_task(function* MoveWidgetsInTwoWindows() { + yield startCustomizing(); + otherWin = yield openAndLoadWindow(null, true); + yield otherWin.PanelUI.ensureReady(); + // Create the XUL button to use in the test in both windows. + createXULButtonForWindow(window); + createXULButtonForWindow(otherWin); + ok(CustomizableUI.inDefaultState, "Should start in default state"); + + for (let widgetId of [kXULWidgetId, kAPIWidgetId]) { + for (let method of ["API", "drag", "dragToItem"]) { + info("Moving widget " + widgetId + " using " + method); + checkToolbar(widgetId, method); + checkPanel(widgetId, method); + checkPalette(widgetId, method); + checkPanel(widgetId, method); + checkToolbar(widgetId, method); + checkPalette(widgetId, method); + } + } + yield promiseWindowClosed(otherWin); + otherWin = null; + yield endCustomizing(); + removeXULButtonForWindow(window); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_876944_customize_mode_create_destroy.js b/browser/components/customizableui/test/browser_876944_customize_mode_create_destroy.js new file mode 100644 index 000000000..ec454dc8d --- /dev/null +++ b/browser/components/customizableui/test/browser_876944_customize_mode_create_destroy.js @@ -0,0 +1,61 @@ +/* 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 kTestWidget1 = "test-customize-mode-create-destroy1"; +const kTestWidget2 = "test-customize-mode-create-destroy2"; + +// Creating and destroying a widget should correctly wrap/unwrap stuff +add_task(function* testWrapUnwrap() { + yield startCustomizing(); + CustomizableUI.createWidget({id: kTestWidget1, label: 'Pretty label', tooltiptext: 'Pretty tooltip'}); + let elem = document.getElementById(kTestWidget1); + let wrapper = document.getElementById("wrapper-" + kTestWidget1); + ok(elem, "There should be an item"); + ok(wrapper, "There should be a wrapper"); + is(wrapper.firstChild.id, kTestWidget1, "Wrapper should have test widget"); + is(wrapper.parentNode.id, "customization-palette", "Wrapper should be in palette"); + CustomizableUI.destroyWidget(kTestWidget1); + wrapper = document.getElementById("wrapper-" + kTestWidget1); + ok(!wrapper, "There should be a wrapper"); + let item = document.getElementById(kTestWidget1); + ok(!item, "There should no longer be an item"); +}); + +// Creating and destroying a widget should correctly deal with panel placeholders +add_task(function* testPanelPlaceholders() { + let panel = document.getElementById(CustomizableUI.AREA_PANEL); + // The value of expectedPlaceholders depends on the default palette layout. + // Bug 1229236 is for these tests to be smarter so the test doesn't need to + // change when the default placements change. + let expectedPlaceholders = 1 + (isInDevEdition() ? 1 : 0); + is(panel.querySelectorAll(".panel-customization-placeholder").length, expectedPlaceholders, "The number of placeholders should be correct."); + CustomizableUI.createWidget({id: kTestWidget2, label: 'Pretty label', tooltiptext: 'Pretty tooltip', defaultArea: CustomizableUI.AREA_PANEL}); + let elem = document.getElementById(kTestWidget2); + let wrapper = document.getElementById("wrapper-" + kTestWidget2); + ok(elem, "There should be an item"); + ok(wrapper, "There should be a wrapper"); + is(wrapper.firstChild.id, kTestWidget2, "Wrapper should have test widget"); + is(wrapper.parentNode, panel, "Wrapper should be in panel"); + expectedPlaceholders = isInDevEdition() ? 1 : 3; + is(panel.querySelectorAll(".panel-customization-placeholder").length, expectedPlaceholders, "The number of placeholders should be correct."); + CustomizableUI.destroyWidget(kTestWidget2); + wrapper = document.getElementById("wrapper-" + kTestWidget2); + ok(!wrapper, "There should be a wrapper"); + let item = document.getElementById(kTestWidget2); + ok(!item, "There should no longer be an item"); + yield endCustomizing(); +}); + +add_task(function* asyncCleanup() { + yield endCustomizing(); + try { + CustomizableUI.destroyWidget(kTestWidget1); + } catch (ex) {} + try { + CustomizableUI.destroyWidget(kTestWidget2); + } catch (ex) {} + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_877006_missing_view.js b/browser/components/customizableui/test/browser_877006_missing_view.js new file mode 100644 index 000000000..a1495c1fe --- /dev/null +++ b/browser/components/customizableui/test/browser_877006_missing_view.js @@ -0,0 +1,41 @@ +/* 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"; + +// Should be able to add broken view widget +add_task(function testAddbrokenViewWidget() { + const kWidgetId = 'test-877006-broken-widget'; + let widgetSpec = { + id: kWidgetId, + type: 'view', + viewId: 'idontexist', + /* Empty handler so we try to attach it maybe? */ + onViewShowing: function() { + } + }; + + let noError = true; + try { + CustomizableUI.createWidget(widgetSpec); + CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_NAVBAR); + } catch (ex) { + Cu.reportError(ex); + noError = false; + } + ok(noError, "Should not throw an exception trying to add a broken view widget."); + + noError = true; + try { + CustomizableUI.destroyWidget(kWidgetId); + } catch (ex) { + Cu.reportError(ex); + noError = false; + } + ok(noError, "Should not throw an exception trying to remove the broken view widget."); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_877178_unregisterArea.js b/browser/components/customizableui/test/browser_877178_unregisterArea.js new file mode 100644 index 000000000..28037787b --- /dev/null +++ b/browser/components/customizableui/test/browser_877178_unregisterArea.js @@ -0,0 +1,50 @@ +/* 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"; + +registerCleanupFunction(removeCustomToolbars); + +// Sanity checks +add_task(function sanityChecks() { + SimpleTest.doesThrow(() => CustomizableUI.registerArea("@foo"), + "Registering areas with an invalid ID should throw."); + + SimpleTest.doesThrow(() => CustomizableUI.registerArea([]), + "Registering areas with an invalid ID should throw."); + + SimpleTest.doesThrow(() => CustomizableUI.unregisterArea("@foo"), + "Unregistering areas with an invalid ID should throw."); + + SimpleTest.doesThrow(() => CustomizableUI.unregisterArea([]), + "Unregistering areas with an invalid ID should throw."); + + SimpleTest.doesThrow(() => CustomizableUI.unregisterArea("unknown"), + "Unregistering an area that's not registered should throw."); +}); + +// Check areas are loaded with their default placements. +add_task(function checkLoadedAres() { + ok(CustomizableUI.inDefaultState, "Everything should be in its default state."); +}); + +// Check registering and unregistering a new area. +add_task(function checkRegisteringAndUnregistering() { + const kToolbarId = "test-registration-toolbar"; + const kButtonId = "test-registration-button"; + createDummyXULButton(kButtonId); + createToolbarWithPlacements(kToolbarId, ["spring", kButtonId, "spring"]); + assertAreaPlacements(kToolbarId, + [/customizableui-special-spring\d+/, + kButtonId, + /customizableui-special-spring\d+/]); + ok(!CustomizableUI.inDefaultState, "With a new toolbar it is no longer in a default state."); + removeCustomToolbars(); // Will call unregisterArea for us + ok(CustomizableUI.inDefaultState, "When the toolbar is unregistered, " + + "everything will return to the default state."); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_877447_skip_missing_ids.js b/browser/components/customizableui/test/browser_877447_skip_missing_ids.js new file mode 100644 index 000000000..0cba7ae4f --- /dev/null +++ b/browser/components/customizableui/test/browser_877447_skip_missing_ids.js @@ -0,0 +1,25 @@ +/* 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"; + +registerCleanupFunction(removeCustomToolbars); + +add_task(function skipMissingIDS() { + const kButtonId = "look-at-me-disappear-button"; + CustomizableUI.reset(); + ok(CustomizableUI.inDefaultState, "Should be in the default state."); + let btn = createDummyXULButton(kButtonId, "Gone!"); + CustomizableUI.addWidgetToArea(kButtonId, CustomizableUI.AREA_NAVBAR); + ok(!CustomizableUI.inDefaultState, "Should no longer be in the default state."); + is(btn.parentNode.parentNode.id, CustomizableUI.AREA_NAVBAR, "Button should be in navbar"); + btn.remove(); + is(btn.parentNode, null, "Button is no longer in the navbar"); + ok(CustomizableUI.inDefaultState, "Should be back in the default state, " + + "despite unknown button ID in placements."); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_878452_drag_to_panel.js b/browser/components/customizableui/test/browser_878452_drag_to_panel.js new file mode 100644 index 000000000..8a8d82294 --- /dev/null +++ b/browser/components/customizableui/test/browser_878452_drag_to_panel.js @@ -0,0 +1,65 @@ +/* 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"; + +// Dragging an item from the palette to another button in the panel should work. +add_task(function*() { + yield startCustomizing(); + let btn = document.getElementById("feed-button"); + let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL); + + let lastButtonIndex = placements.length - 1; + let lastButton = placements[lastButtonIndex]; + let placementsAfterInsert = placements.slice(0, lastButtonIndex).concat(["feed-button", lastButton]); + let lastButtonNode = document.getElementById(lastButton); + simulateItemDrag(btn, lastButtonNode); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterInsert); + ok(!CustomizableUI.inDefaultState, "Should no longer be in default state."); + let palette = document.getElementById("customization-palette"); + simulateItemDrag(btn, palette); + ok(CustomizableUI.inDefaultState, "Should be in default state again."); +}); + +// Dragging an item from the palette to the panel itself should also work. +add_task(function*() { + yield startCustomizing(); + let btn = document.getElementById("feed-button"); + let panel = document.getElementById(CustomizableUI.AREA_PANEL); + let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL); + + let placementsAfterAppend = placements.concat(["feed-button"]); + simulateItemDrag(btn, panel); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterAppend); + ok(!CustomizableUI.inDefaultState, "Should no longer be in default state."); + let palette = document.getElementById("customization-palette"); + simulateItemDrag(btn, palette); + ok(CustomizableUI.inDefaultState, "Should be in default state again."); +}); + +// Dragging an item from the palette to an empty panel should also work. +add_task(function*() { + let widgetIds = getAreaWidgetIds(CustomizableUI.AREA_PANEL); + while (widgetIds.length) { + CustomizableUI.removeWidgetFromArea(widgetIds.shift()); + } + yield startCustomizing(); + let btn = document.getElementById("feed-button"); + let panel = document.getElementById(CustomizableUI.AREA_PANEL); + + assertAreaPlacements(panel.id, []); + + let placementsAfterAppend = ["feed-button"]; + simulateItemDrag(btn, panel); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterAppend); + ok(!CustomizableUI.inDefaultState, "Should no longer be in default state."); + let palette = document.getElementById("customization-palette"); + simulateItemDrag(btn, palette); + assertAreaPlacements(panel.id, []); +}); + +add_task(function* asyncCleanup() { + yield endCustomizing(); + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_880164_customization_context_menus.js b/browser/components/customizableui/test/browser_880164_customization_context_menus.js new file mode 100644 index 000000000..57a0db773 --- /dev/null +++ b/browser/components/customizableui/test/browser_880164_customization_context_menus.js @@ -0,0 +1,414 @@ +/* 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"; + +requestLongerTimeout(2); + +const isOSX = (Services.appinfo.OS === "Darwin"); + +// Right-click on the home button should +// show a context menu with options to move it. +add_task(function*() { + let contextMenu = document.getElementById("toolbar-context-menu"); + let shownPromise = popupShown(contextMenu); + let homeButton = document.getElementById("home-button"); + EventUtils.synthesizeMouse(homeButton, 2, 2, {type: "contextmenu", button: 2 }); + yield shownPromise; + + let expectedEntries = [ + [".customize-context-moveToPanel", true], + [".customize-context-removeFromToolbar", true], + ["---"] + ]; + if (!isOSX) { + expectedEntries.push(["#toggle_toolbar-menubar", true]); + } + expectedEntries.push( + ["#toggle_PersonalToolbar", true], + ["---"], + [".viewCustomizeToolbar", true] + ); + checkContextMenu(contextMenu, expectedEntries); + + let hiddenPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + yield hiddenPromise; +}); + +// Right-click on an empty bit of tabstrip should +// show a context menu without options to move it, +// but with tab-specific options instead. +add_task(function*() { + // ensure there are tabs to reload/bookmark: + let extraTab = gBrowser.selectedTab = gBrowser.addTab(); + yield promiseTabLoadEvent(extraTab, "http://example.com/"); + let contextMenu = document.getElementById("toolbar-context-menu"); + let shownPromise = popupShown(contextMenu); + let tabstrip = document.getElementById("tabbrowser-tabs"); + let rect = tabstrip.getBoundingClientRect(); + EventUtils.synthesizeMouse(tabstrip, rect.width - 2, 2, {type: "contextmenu", button: 2 }); + yield shownPromise; + + let closedTabsAvailable = SessionStore.getClosedTabCount(window) == 0; + info("Closed tabs: " + closedTabsAvailable); + let expectedEntries = [ + ["#toolbar-context-reloadAllTabs", true], + ["#toolbar-context-bookmarkAllTabs", true], + ["#toolbar-context-undoCloseTab", !closedTabsAvailable], + ["---"] + ]; + if (!isOSX) { + expectedEntries.push(["#toggle_toolbar-menubar", true]); + } + expectedEntries.push( + ["#toggle_PersonalToolbar", true], + ["---"], + [".viewCustomizeToolbar", true] + ); + checkContextMenu(contextMenu, expectedEntries); + + let hiddenPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + yield hiddenPromise; + gBrowser.removeTab(extraTab); +}); + +// Right-click on an empty bit of extra toolbar should +// show a context menu with moving options disabled, +// and a toggle option for the extra toolbar +add_task(function*() { + let contextMenu = document.getElementById("toolbar-context-menu"); + let shownPromise = popupShown(contextMenu); + let toolbar = createToolbarWithPlacements("880164_empty_toolbar", []); + toolbar.setAttribute("context", "toolbar-context-menu"); + toolbar.setAttribute("toolbarname", "Fancy Toolbar for Context Menu"); + EventUtils.synthesizeMouseAtCenter(toolbar, {type: "contextmenu", button: 2 }); + yield shownPromise; + + let expectedEntries = [ + [".customize-context-moveToPanel", false], + [".customize-context-removeFromToolbar", false], + ["---"] + ]; + if (!isOSX) { + expectedEntries.push(["#toggle_toolbar-menubar", true]); + } + expectedEntries.push( + ["#toggle_PersonalToolbar", true], + ["#toggle_880164_empty_toolbar", true], + ["---"], + [".viewCustomizeToolbar", true] + ); + checkContextMenu(contextMenu, expectedEntries); + + let hiddenPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + yield hiddenPromise; + removeCustomToolbars(); +}); + + +// Right-click on the urlbar-container should +// show a context menu with disabled options to move it. +add_task(function*() { + let contextMenu = document.getElementById("toolbar-context-menu"); + let shownPromise = popupShown(contextMenu); + let urlBarContainer = document.getElementById("urlbar-container"); + // Need to make sure not to click within an edit field. + EventUtils.synthesizeMouse(urlBarContainer, 100, 1, {type: "contextmenu", button: 2 }); + yield shownPromise; + + let expectedEntries = [ + [".customize-context-moveToPanel", false], + [".customize-context-removeFromToolbar", false], + ["---"] + ]; + if (!isOSX) { + expectedEntries.push(["#toggle_toolbar-menubar", true]); + } + expectedEntries.push( + ["#toggle_PersonalToolbar", true], + ["---"], + [".viewCustomizeToolbar", true] + ); + checkContextMenu(contextMenu, expectedEntries); + + let hiddenPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + yield hiddenPromise; +}); + +// Right-click on the searchbar and moving it to the menu +// and back should move the search-container instead. +add_task(function*() { + let searchbar = document.getElementById("searchbar"); + gCustomizeMode.addToPanel(searchbar); + let placement = CustomizableUI.getPlacementOfWidget("search-container"); + is(placement.area, CustomizableUI.AREA_PANEL, "Should be in panel"); + + let shownPanelPromise = promisePanelShown(window); + PanelUI.toggle({type: "command"}); + yield shownPanelPromise; + let hiddenPanelPromise = promisePanelHidden(window); + PanelUI.toggle({type: "command"}); + yield hiddenPanelPromise; + + gCustomizeMode.addToToolbar(searchbar); + placement = CustomizableUI.getPlacementOfWidget("search-container"); + is(placement.area, CustomizableUI.AREA_NAVBAR, "Should be in navbar"); + gCustomizeMode.removeFromArea(searchbar); + placement = CustomizableUI.getPlacementOfWidget("search-container"); + is(placement, null, "Should be in palette"); + CustomizableUI.reset(); + placement = CustomizableUI.getPlacementOfWidget("search-container"); + is(placement.area, CustomizableUI.AREA_NAVBAR, "Should be in navbar"); +}); + +// Right-click on an item within the menu panel should +// show a context menu with options to move it. +add_task(function*() { + let shownPanelPromise = promisePanelShown(window); + PanelUI.toggle({type: "command"}); + yield shownPanelPromise; + + let contextMenu = document.getElementById("customizationPanelItemContextMenu"); + let shownContextPromise = popupShown(contextMenu); + let newWindowButton = document.getElementById("new-window-button"); + ok(newWindowButton, "new-window-button was found"); + EventUtils.synthesizeMouse(newWindowButton, 2, 2, {type: "contextmenu", button: 2}); + yield shownContextPromise; + + is(PanelUI.panel.state, "open", "The PanelUI should still be open."); + + let expectedEntries = [ + [".customize-context-moveToToolbar", true], + [".customize-context-removeFromPanel", true], + ["---"], + [".viewCustomizeToolbar", true] + ]; + checkContextMenu(contextMenu, expectedEntries); + + let hiddenContextPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + yield hiddenContextPromise; + + let hiddenPromise = promisePanelHidden(window); + PanelUI.toggle({type: "command"}); + yield hiddenPromise; +}); + +// Right-click on the home button while in customization mode +// should show a context menu with options to move it. +add_task(function*() { + yield startCustomizing(); + let contextMenu = document.getElementById("toolbar-context-menu"); + let shownPromise = popupShown(contextMenu); + let homeButton = document.getElementById("wrapper-home-button"); + EventUtils.synthesizeMouse(homeButton, 2, 2, {type: "contextmenu", button: 2}); + yield shownPromise; + + let expectedEntries = [ + [".customize-context-moveToPanel", true], + [".customize-context-removeFromToolbar", true], + ["---"] + ]; + if (!isOSX) { + expectedEntries.push(["#toggle_toolbar-menubar", true]); + } + expectedEntries.push( + ["#toggle_PersonalToolbar", true], + ["---"], + [".viewCustomizeToolbar", false] + ); + checkContextMenu(contextMenu, expectedEntries); + + let hiddenContextPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + yield hiddenContextPromise; +}); + +// Right-click on an item in the palette should +// show a context menu with options to move it. +add_task(function*() { + let contextMenu = document.getElementById("customizationPaletteItemContextMenu"); + let shownPromise = popupShown(contextMenu); + let openFileButton = document.getElementById("wrapper-open-file-button"); + EventUtils.synthesizeMouse(openFileButton, 2, 2, {type: "contextmenu", button: 2}); + yield shownPromise; + + let expectedEntries = [ + [".customize-context-addToToolbar", true], + [".customize-context-addToPanel", true] + ]; + checkContextMenu(contextMenu, expectedEntries); + + let hiddenContextPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + yield hiddenContextPromise; +}); + +// Right-click on an item in the panel while in customization mode +// should show a context menu with options to move it. +add_task(function*() { + let contextMenu = document.getElementById("customizationPanelItemContextMenu"); + let shownPromise = popupShown(contextMenu); + let newWindowButton = document.getElementById("wrapper-new-window-button"); + EventUtils.synthesizeMouse(newWindowButton, 2, 2, {type: "contextmenu", button: 2}); + yield shownPromise; + + let expectedEntries = [ + [".customize-context-moveToToolbar", true], + [".customize-context-removeFromPanel", true], + ["---"], + [".viewCustomizeToolbar", false] + ]; + checkContextMenu(contextMenu, expectedEntries); + + let hiddenContextPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + yield hiddenContextPromise; + yield endCustomizing(); +}); + +// Test the toolbarbutton panel context menu in customization mode +// without opening the panel before customization mode +add_task(function*() { + this.otherWin = yield openAndLoadWindow(null, true); + + yield new Promise(resolve => waitForFocus(resolve, this.otherWin)); + + yield startCustomizing(this.otherWin); + + let contextMenu = this.otherWin.document.getElementById("customizationPanelItemContextMenu"); + let shownPromise = popupShown(contextMenu); + let newWindowButton = this.otherWin.document.getElementById("wrapper-new-window-button"); + EventUtils.synthesizeMouse(newWindowButton, 2, 2, {type: "contextmenu", button: 2}, this.otherWin); + yield shownPromise; + + let expectedEntries = [ + [".customize-context-moveToToolbar", true], + [".customize-context-removeFromPanel", true], + ["---"], + [".viewCustomizeToolbar", false] + ]; + checkContextMenu(contextMenu, expectedEntries, this.otherWin); + + let hiddenContextPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + yield hiddenContextPromise; + yield endCustomizing(this.otherWin); + yield promiseWindowClosed(this.otherWin); + this.otherWin = null; + + yield new Promise(resolve => waitForFocus(resolve, window)); +}); + +// Bug 945191 - Combined buttons show wrong context menu options +// when they are in the toolbar. +add_task(function*() { + yield startCustomizing(); + let contextMenu = document.getElementById("customizationPanelItemContextMenu"); + let shownPromise = popupShown(contextMenu); + let zoomControls = document.getElementById("wrapper-zoom-controls"); + EventUtils.synthesizeMouse(zoomControls, 2, 2, {type: "contextmenu", button: 2}); + yield shownPromise; + // Execute the command to move the item from the panel to the toolbar. + contextMenu.childNodes[0].doCommand(); + let hiddenPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + yield hiddenPromise; + yield endCustomizing(); + + zoomControls = document.getElementById("zoom-controls"); + is(zoomControls.parentNode.id, "nav-bar-customization-target", "Zoom-controls should be on the nav-bar"); + + contextMenu = document.getElementById("toolbar-context-menu"); + shownPromise = popupShown(contextMenu); + EventUtils.synthesizeMouse(zoomControls, 2, 2, {type: "contextmenu", button: 2}); + yield shownPromise; + + let expectedEntries = [ + [".customize-context-moveToPanel", true], + [".customize-context-removeFromToolbar", true], + ["---"] + ]; + if (!isOSX) { + expectedEntries.push(["#toggle_toolbar-menubar", true]); + } + expectedEntries.push( + ["#toggle_PersonalToolbar", true], + ["---"], + [".viewCustomizeToolbar", true] + ); + checkContextMenu(contextMenu, expectedEntries); + + hiddenPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + yield hiddenPromise; + yield resetCustomization(); +}); + +// Bug 947586 - After customization, panel items show wrong context menu options +add_task(function*() { + yield startCustomizing(); + yield endCustomizing(); + + yield PanelUI.show(); + + let contextMenu = document.getElementById("customizationPanelItemContextMenu"); + let shownContextPromise = popupShown(contextMenu); + let newWindowButton = document.getElementById("new-window-button"); + ok(newWindowButton, "new-window-button was found"); + EventUtils.synthesizeMouse(newWindowButton, 2, 2, {type: "contextmenu", button: 2}); + yield shownContextPromise; + + is(PanelUI.panel.state, "open", "The PanelUI should still be open."); + + let expectedEntries = [ + [".customize-context-moveToToolbar", true], + [".customize-context-removeFromPanel", true], + ["---"], + [".viewCustomizeToolbar", true] + ]; + checkContextMenu(contextMenu, expectedEntries); + + let hiddenContextPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + yield hiddenContextPromise; + + let hiddenPromise = promisePanelHidden(window); + PanelUI.hide(); + yield hiddenPromise; +}); + + +// Bug 982027 - moving icon around removes custom context menu. +add_task(function*() { + let widgetId = "custom-context-menu-toolbarbutton"; + let expectedContext = "myfancycontext"; + let widget = createDummyXULButton(widgetId, "Test ctxt menu"); + widget.setAttribute("context", expectedContext); + CustomizableUI.addWidgetToArea(widgetId, CustomizableUI.AREA_NAVBAR); + is(widget.getAttribute("context"), expectedContext, "Should have context menu when added to the toolbar."); + + yield startCustomizing(); + is(widget.getAttribute("context"), "", "Should not have own context menu in the toolbar now that we're customizing."); + is(widget.getAttribute("wrapped-context"), expectedContext, "Should keep own context menu wrapped when in toolbar."); + + let panel = PanelUI.contents; + simulateItemDrag(widget, panel); + is(widget.getAttribute("context"), "", "Should not have own context menu when in the panel."); + is(widget.getAttribute("wrapped-context"), expectedContext, "Should keep own context menu wrapped now that we're in the panel."); + + simulateItemDrag(widget, document.getElementById("nav-bar").customizationTarget); + is(widget.getAttribute("context"), "", "Should not have own context menu when back in toolbar because we're still customizing."); + is(widget.getAttribute("wrapped-context"), expectedContext, "Should keep own context menu wrapped now that we're back in the toolbar."); + + yield endCustomizing(); + is(widget.getAttribute("context"), expectedContext, "Should have context menu again now that we're out of customize mode."); + CustomizableUI.removeWidgetFromArea(widgetId); + widget.remove(); + ok(CustomizableUI.inDefaultState, "Should be in default state after removing button."); +}); diff --git a/browser/components/customizableui/test/browser_880382_drag_wide_widgets_in_panel.js b/browser/components/customizableui/test/browser_880382_drag_wide_widgets_in_panel.js new file mode 100644 index 000000000..9057d0557 --- /dev/null +++ b/browser/components/customizableui/test/browser_880382_drag_wide_widgets_in_panel.js @@ -0,0 +1,497 @@ +/* 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"; + +requestLongerTimeout(5); + +// Dragging the zoom controls to be before the print button should not move any controls. +add_task(function*() { + yield startCustomizing(); + let zoomControls = document.getElementById("zoom-controls"); + let printButton = document.getElementById("print-button"); + let placementsAfterMove = ["edit-controls", + "new-window-button", + "privatebrowsing-button", + "save-page-button", + "zoom-controls", + "print-button", + "history-panelmenu", + "fullscreen-button", + "find-button", + "preferences-button", + "add-ons-button", + "developer-button", + "sync-button", + ]; + removeDeveloperButtonIfDevEdition(placementsAfterMove); + simulateItemDrag(zoomControls, printButton); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove); + ok(!CustomizableUI.inDefaultState, "Should no longer be in default state."); + let newWindowButton = document.getElementById("new-window-button"); + simulateItemDrag(zoomControls, newWindowButton); + ok(CustomizableUI.inDefaultState, "Should be in default state again."); +}); + +// Dragging the zoom controls to be before the save button should not move any controls. +add_task(function*() { + yield startCustomizing(); + let zoomControls = document.getElementById("zoom-controls"); + let savePageButton = document.getElementById("save-page-button"); + let placementsAfterMove = ["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", + "developer-button", + "sync-button", + ]; + removeDeveloperButtonIfDevEdition(placementsAfterMove); + simulateItemDrag(zoomControls, savePageButton); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove); + ok(CustomizableUI.inDefaultState, "Should be in default state."); +}); + + +// Dragging the zoom controls to be before the new-window button should not move any widgets. +add_task(function*() { + yield startCustomizing(); + let zoomControls = document.getElementById("zoom-controls"); + let newWindowButton = document.getElementById("new-window-button"); + let placementsAfterMove = ["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", + "developer-button", + "sync-button", + ]; + removeDeveloperButtonIfDevEdition(placementsAfterMove); + simulateItemDrag(zoomControls, newWindowButton); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove); + ok(CustomizableUI.inDefaultState, "Should still be in default state."); +}); + +// Dragging the zoom controls to be before the history-panelmenu should move the zoom-controls in to the row higher than the history-panelmenu. +add_task(function*() { + yield startCustomizing(); + let zoomControls = document.getElementById("zoom-controls"); + let historyPanelMenu = document.getElementById("history-panelmenu"); + let placementsAfterMove = ["edit-controls", + "new-window-button", + "privatebrowsing-button", + "save-page-button", + "zoom-controls", + "print-button", + "history-panelmenu", + "fullscreen-button", + "find-button", + "preferences-button", + "add-ons-button", + "developer-button", + "sync-button", + ]; + removeDeveloperButtonIfDevEdition(placementsAfterMove); + simulateItemDrag(zoomControls, historyPanelMenu); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove); + ok(!CustomizableUI.inDefaultState, "Should no longer be in default state."); + let newWindowButton = document.getElementById("new-window-button"); + simulateItemDrag(zoomControls, newWindowButton); + ok(CustomizableUI.inDefaultState, "Should be in default state again."); +}); + +// Dragging the zoom controls to be before the preferences-button should move the zoom-controls +// in to the row higher than the preferences-button. +add_task(function*() { + yield startCustomizing(); + let zoomControls = document.getElementById("zoom-controls"); + let preferencesButton = document.getElementById("preferences-button"); + let placementsAfterMove = ["edit-controls", + "new-window-button", + "privatebrowsing-button", + "save-page-button", + "print-button", + "history-panelmenu", + "fullscreen-button", + "zoom-controls", + "find-button", + "preferences-button", + "add-ons-button", + "developer-button", + "sync-button", + ]; + removeDeveloperButtonIfDevEdition(placementsAfterMove); + simulateItemDrag(zoomControls, preferencesButton); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove); + ok(!CustomizableUI.inDefaultState, "Should no longer be in default state."); + let newWindowButton = document.getElementById("new-window-button"); + simulateItemDrag(zoomControls, newWindowButton); + ok(CustomizableUI.inDefaultState, "Should be in default state again."); +}); + +// Dragging an item from the palette to before the zoom-controls should move it and two other buttons before the zoom controls. +add_task(function*() { + yield startCustomizing(); + let openFileButton = document.getElementById("open-file-button"); + let zoomControls = document.getElementById("zoom-controls"); + let placementsAfterInsert = ["edit-controls", + "open-file-button", + "new-window-button", + "privatebrowsing-button", + "zoom-controls", + "save-page-button", + "print-button", + "history-panelmenu", + "fullscreen-button", + "find-button", + "preferences-button", + "add-ons-button", + "developer-button", + "sync-button", + ]; + removeDeveloperButtonIfDevEdition(placementsAfterInsert); + simulateItemDrag(openFileButton, zoomControls); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterInsert); + ok(!CustomizableUI.inDefaultState, "Should no longer be in default state."); + let palette = document.getElementById("customization-palette"); + // Check that the palette items are re-wrapped correctly. + let feedWrapper = document.getElementById("wrapper-feed-button"); + let feedButton = document.getElementById("feed-button"); + is(feedButton.parentNode, feedWrapper, + "feed-button should be a child of wrapper-feed-button"); + is(feedWrapper.getAttribute("place"), "palette", + "The feed-button wrapper should have it's place set to 'palette'"); + simulateItemDrag(openFileButton, palette); + is(openFileButton.parentNode.tagName, "toolbarpaletteitem", + "The open-file-button should be wrapped by a toolbarpaletteitem"); + let newWindowButton = document.getElementById("new-window-button"); + simulateItemDrag(zoomControls, newWindowButton); + ok(CustomizableUI.inDefaultState, "Should be in default state again."); +}); + +// Dragging an item from the palette to before the edit-controls +// should move it and two other buttons before the edit and zoom controls. +add_task(function*() { + yield startCustomizing(); + let openFileButton = document.getElementById("open-file-button"); + let editControls = document.getElementById("edit-controls"); + let placementsAfterInsert = ["open-file-button", + "new-window-button", + "privatebrowsing-button", + "edit-controls", + "zoom-controls", + "save-page-button", + "print-button", + "history-panelmenu", + "fullscreen-button", + "find-button", + "preferences-button", + "add-ons-button", + "developer-button", + "sync-button", + ]; + removeDeveloperButtonIfDevEdition(placementsAfterInsert); + simulateItemDrag(openFileButton, editControls); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterInsert); + ok(!CustomizableUI.inDefaultState, "Should no longer be in default state."); + let palette = document.getElementById("customization-palette"); + // Check that the palette items are re-wrapped correctly. + let feedWrapper = document.getElementById("wrapper-feed-button"); + let feedButton = document.getElementById("feed-button"); + is(feedButton.parentNode, feedWrapper, + "feed-button should be a child of wrapper-feed-button"); + is(feedWrapper.getAttribute("place"), "palette", + "The feed-button wrapper should have it's place set to 'palette'"); + simulateItemDrag(openFileButton, palette); + is(openFileButton.parentNode.tagName, "toolbarpaletteitem", + "The open-file-button should be wrapped by a toolbarpaletteitem"); + ok(CustomizableUI.inDefaultState, "Should be in default state again."); +}); + +// Dragging the edit-controls to be before the zoom-controls button +// should not move any widgets. +add_task(function*() { + yield startCustomizing(); + let editControls = document.getElementById("edit-controls"); + let zoomControls = document.getElementById("zoom-controls"); + let placementsAfterMove = ["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", + "developer-button", + "sync-button", + ]; + removeDeveloperButtonIfDevEdition(placementsAfterMove); + simulateItemDrag(editControls, zoomControls); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove); + ok(CustomizableUI.inDefaultState, "Should still be in default state."); +}); + +// Dragging the edit-controls to be before the new-window-button should +// move the zoom-controls before the edit-controls. +add_task(function*() { + yield startCustomizing(); + let editControls = document.getElementById("edit-controls"); + let newWindowButton = document.getElementById("new-window-button"); + let placementsAfterMove = ["zoom-controls", + "edit-controls", + "new-window-button", + "privatebrowsing-button", + "save-page-button", + "print-button", + "history-panelmenu", + "fullscreen-button", + "find-button", + "preferences-button", + "add-ons-button", + "developer-button", + "sync-button", + ]; + removeDeveloperButtonIfDevEdition(placementsAfterMove); + simulateItemDrag(editControls, newWindowButton); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove); + let zoomControls = document.getElementById("zoom-controls"); + simulateItemDrag(editControls, zoomControls); + ok(CustomizableUI.inDefaultState, "Should still be in default state."); +}); + +// Dragging the edit-controls to be before the privatebrowsing-button +// should move the edit-controls in to the row higher than the +// privatebrowsing-button. +add_task(function*() { + yield startCustomizing(); + let editControls = document.getElementById("edit-controls"); + let privateBrowsingButton = document.getElementById("privatebrowsing-button"); + let placementsAfterMove = ["zoom-controls", + "edit-controls", + "new-window-button", + "privatebrowsing-button", + "save-page-button", + "print-button", + "history-panelmenu", + "fullscreen-button", + "find-button", + "preferences-button", + "add-ons-button", + "developer-button", + "sync-button", + ]; + removeDeveloperButtonIfDevEdition(placementsAfterMove); + simulateItemDrag(editControls, privateBrowsingButton); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove); + let zoomControls = document.getElementById("zoom-controls"); + simulateItemDrag(editControls, zoomControls); + ok(CustomizableUI.inDefaultState, "Should still be in default state."); +}); + +// Dragging the edit-controls to be before the save-page-button +// should move the edit-controls in to the row higher than the +// save-page-button. +add_task(function*() { + yield startCustomizing(); + let editControls = document.getElementById("edit-controls"); + let savePageButton = document.getElementById("save-page-button"); + let placementsAfterMove = ["zoom-controls", + "edit-controls", + "new-window-button", + "privatebrowsing-button", + "save-page-button", + "print-button", + "history-panelmenu", + "fullscreen-button", + "find-button", + "preferences-button", + "add-ons-button", + "developer-button", + "sync-button", + ]; + removeDeveloperButtonIfDevEdition(placementsAfterMove); + simulateItemDrag(editControls, savePageButton); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove); + let zoomControls = document.getElementById("zoom-controls"); + simulateItemDrag(editControls, zoomControls); + ok(CustomizableUI.inDefaultState, "Should still be in default state."); +}); + +// Dragging the edit-controls to the panel itself should append +// the edit controls to the bottom of the panel. +add_task(function*() { + yield startCustomizing(); + let editControls = document.getElementById("edit-controls"); + let panel = document.getElementById(CustomizableUI.AREA_PANEL); + let placementsAfterMove = ["zoom-controls", + "new-window-button", + "privatebrowsing-button", + "save-page-button", + "print-button", + "history-panelmenu", + "fullscreen-button", + "find-button", + "preferences-button", + "add-ons-button", + "edit-controls", + "developer-button", + "sync-button", + ]; + removeDeveloperButtonIfDevEdition(placementsAfterMove); + simulateItemDrag(editControls, panel); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove); + let zoomControls = document.getElementById("zoom-controls"); + simulateItemDrag(editControls, zoomControls); + ok(CustomizableUI.inDefaultState, "Should still be in default state."); +}); + +// Dragging the edit-controls to the customization-palette and +// back should work. +add_task(function*() { + yield startCustomizing(); + let editControls = document.getElementById("edit-controls"); + let palette = document.getElementById("customization-palette"); + let placementsAfterMove = ["zoom-controls", + "new-window-button", + "privatebrowsing-button", + "save-page-button", + "print-button", + "history-panelmenu", + "fullscreen-button", + "find-button", + "preferences-button", + "add-ons-button", + "developer-button", + "sync-button", + ]; + removeDeveloperButtonIfDevEdition(placementsAfterMove); + let paletteChildElementCount = palette.childElementCount; + simulateItemDrag(editControls, palette); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove); + is(paletteChildElementCount + 1, palette.childElementCount, + "The palette should have a new child, congratulations!"); + is(editControls.parentNode.id, "wrapper-edit-controls", + "The edit-controls should be properly wrapped."); + is(editControls.parentNode.getAttribute("place"), "palette", + "The edit-controls should have the place of 'palette'."); + let zoomControls = document.getElementById("zoom-controls"); + simulateItemDrag(editControls, zoomControls); + is(paletteChildElementCount, palette.childElementCount, + "The palette child count should have returned to its prior value."); + ok(CustomizableUI.inDefaultState, "Should still be in default state."); +}); + +// Dragging the edit-controls to each of the panel placeholders +// should append the edit-controls to the bottom of the panel. +add_task(function*() { + yield startCustomizing(); + let editControls = document.getElementById("edit-controls"); + let panel = document.getElementById(CustomizableUI.AREA_PANEL); + let numPlaceholders = 2; + for (let i = 0; i < numPlaceholders; i++) { + // This test relies on there being a specific number of widgets in the + // panel. The addition of sync-button screwed this up, so we remove it + // here. We should either fix the tests to not rely on the specific layout, + // or fix bug 1007910 which would change the placeholder logic in different + // ways. Bug 1229236 is for these tests to be smarter. + CustomizableUI.removeWidgetFromArea("sync-button"); + // NB: We can't just iterate over all of the placeholders + // because each drag-drop action recreates them. + let placeholder = panel.getElementsByClassName("panel-customization-placeholder")[i]; + let placementsAfterMove = ["zoom-controls", + "new-window-button", + "privatebrowsing-button", + "save-page-button", + "print-button", + "history-panelmenu", + "fullscreen-button", + "find-button", + "preferences-button", + "add-ons-button", + "edit-controls", + "developer-button"]; + removeDeveloperButtonIfDevEdition(placementsAfterMove); + simulateItemDrag(editControls, placeholder); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove); + let zoomControls = document.getElementById("zoom-controls"); + simulateItemDrag(editControls, zoomControls); + CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL); + ok(CustomizableUI.inDefaultState, "Should still be in default state."); + } +}); + +// Dragging the open-file-button back on to itself should work. +add_task(function*() { + yield startCustomizing(); + let openFileButton = document.getElementById("open-file-button"); + is(openFileButton.parentNode.tagName, "toolbarpaletteitem", + "open-file-button should be wrapped by a toolbarpaletteitem"); + simulateItemDrag(openFileButton, openFileButton); + is(openFileButton.parentNode.tagName, "toolbarpaletteitem", + "open-file-button should be wrapped by a toolbarpaletteitem"); + let editControls = document.getElementById("edit-controls"); + is(editControls.parentNode.tagName, "toolbarpaletteitem", + "edit-controls should be wrapped by a toolbarpaletteitem"); + ok(CustomizableUI.inDefaultState, "Should still be in default state."); +}); + +// Dragging a small button onto the last big button should work. +add_task(function*() { + // Bug 1007910 requires there be a placeholder on the final row for this + // test to work as written. The addition of sync-button meant that's not true + // so we remove it from here. Bug 1229236 is for these tests to be smarter. + CustomizableUI.removeWidgetFromArea("sync-button"); + yield startCustomizing(); + let editControls = document.getElementById("edit-controls"); + let panel = document.getElementById(CustomizableUI.AREA_PANEL); + let target = panel.getElementsByClassName("panel-customization-placeholder")[0]; + let placementsAfterMove = ["zoom-controls", + "new-window-button", + "privatebrowsing-button", + "save-page-button", + "print-button", + "history-panelmenu", + "fullscreen-button", + "find-button", + "preferences-button", + "add-ons-button", + "edit-controls", + "developer-button"]; + removeDeveloperButtonIfDevEdition(placementsAfterMove); + simulateItemDrag(editControls, target); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove); + let itemToDrag = "email-link-button"; // any button in the palette by default. + let button = document.getElementById(itemToDrag); + placementsAfterMove.splice(11, 0, itemToDrag); + simulateItemDrag(button, editControls); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterMove); + + // Put stuff back: + let palette = document.getElementById("customization-palette"); + let zoomControls = document.getElementById("zoom-controls"); + simulateItemDrag(button, palette); + simulateItemDrag(editControls, zoomControls); + CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL); + ok(CustomizableUI.inDefaultState, "Should be in default state again."); +}); + +add_task(function* asyncCleanup() { + yield endCustomizing(); + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_884402_customize_from_overflow.js b/browser/components/customizableui/test/browser_884402_customize_from_overflow.js new file mode 100644 index 000000000..f50767c06 --- /dev/null +++ b/browser/components/customizableui/test/browser_884402_customize_from_overflow.js @@ -0,0 +1,81 @@ +"use strict"; + +var overflowPanel = document.getElementById("widget-overflow"); + +const isOSX = (Services.appinfo.OS === "Darwin"); + +var originalWindowWidth; +registerCleanupFunction(function() { + overflowPanel.removeAttribute("animate"); + window.resizeTo(originalWindowWidth, window.outerHeight); +}); + +// Right-click on an item within the overflow panel should +// show a context menu with options to move it. +add_task(function*() { + + overflowPanel.setAttribute("animate", "false"); + + originalWindowWidth = window.outerWidth; + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar."); + window.resizeTo(400, window.outerHeight); + + yield waitForCondition(() => navbar.hasAttribute("overflowing")); + ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + + let chevron = document.getElementById("nav-bar-overflow-button"); + let shownPanelPromise = promisePanelElementShown(window, overflowPanel); + chevron.click(); + yield shownPanelPromise; + + let contextMenu = document.getElementById("toolbar-context-menu"); + let shownContextPromise = popupShown(contextMenu); + let homeButton = document.getElementById("home-button"); + ok(homeButton, "home-button was found"); + is(homeButton.getAttribute("overflowedItem"), "true", "Home button is overflowing"); + EventUtils.synthesizeMouse(homeButton, 2, 2, {type: "contextmenu", button: 2}); + yield shownContextPromise; + + is(overflowPanel.state, "open", "The widget overflow panel should still be open."); + + let expectedEntries = [ + [".customize-context-moveToPanel", true], + [".customize-context-removeFromToolbar", true], + ["---"] + ]; + if (!isOSX) { + expectedEntries.push(["#toggle_toolbar-menubar", true]); + } + expectedEntries.push( + ["#toggle_PersonalToolbar", true], + ["---"], + [".viewCustomizeToolbar", true] + ); + checkContextMenu(contextMenu, expectedEntries); + + let hiddenContextPromise = popupHidden(contextMenu); + let hiddenPromise = promisePanelElementHidden(window, overflowPanel); + let moveToPanel = contextMenu.querySelector(".customize-context-moveToPanel"); + if (moveToPanel) { + moveToPanel.click(); + } + contextMenu.hidePopup(); + yield hiddenContextPromise; + yield hiddenPromise; + + let homeButtonPlacement = CustomizableUI.getPlacementOfWidget("home-button"); + ok(homeButtonPlacement, "Home button should still have a placement"); + is(homeButtonPlacement && homeButtonPlacement.area, "PanelUI-contents", "Home button should be in the panel now"); + CustomizableUI.reset(); + + // In some cases, it can take a tick for the navbar to overflow again. Wait for it: + yield waitForCondition(() => navbar.hasAttribute("overflowing")); + ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + + homeButtonPlacement = CustomizableUI.getPlacementOfWidget("home-button"); + ok(homeButtonPlacement, "Home button should still have a placement"); + is(homeButtonPlacement && homeButtonPlacement.area, "nav-bar", "Home button should be back in the navbar now"); + + is(homeButton.getAttribute("overflowedItem"), "true", "Home button should still be overflowed"); +}); diff --git a/browser/components/customizableui/test/browser_885052_customize_mode_observers_disabed.js b/browser/components/customizableui/test/browser_885052_customize_mode_observers_disabed.js new file mode 100644 index 000000000..ea6f5a4e3 --- /dev/null +++ b/browser/components/customizableui/test/browser_885052_customize_mode_observers_disabed.js @@ -0,0 +1,45 @@ +/* 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"; + +function isFullscreenSizeMode() { + let sizemode = document.documentElement.getAttribute("sizemode"); + return sizemode == "fullscreen"; +} + +// Observers should be disabled when in customization mode. +add_task(function*() { + // Open and close the panel to make sure that the + // area is generated before getting a child of the area. + let shownPanelPromise = promisePanelShown(window); + PanelUI.toggle({type: "command"}); + yield shownPanelPromise; + let hiddenPanelPromise = promisePanelHidden(window); + PanelUI.toggle({type: "command"}); + yield hiddenPanelPromise; + + let fullscreenButton = document.getElementById("fullscreen-button"); + ok(!fullscreenButton.checked, "Fullscreen button should not be checked when not in fullscreen.") + ok(!isFullscreenSizeMode(), "Should not be in fullscreen sizemode before we enter fullscreen."); + + BrowserFullScreen(); + yield waitForCondition(() => isFullscreenSizeMode()); + ok(fullscreenButton.checked, "Fullscreen button should be checked when in fullscreen.") + + yield startCustomizing(); + + let fullscreenButtonWrapper = document.getElementById("wrapper-fullscreen-button"); + ok(fullscreenButtonWrapper.hasAttribute("itemobserves"), "Observer should be moved to wrapper"); + fullscreenButton = document.getElementById("fullscreen-button"); + ok(!fullscreenButton.hasAttribute("observes"), "Observer should be removed from button"); + ok(!fullscreenButton.checked, "Fullscreen button should no longer be checked during customization mode"); + + yield endCustomizing(); + + BrowserFullScreen(); + fullscreenButton = document.getElementById("fullscreen-button"); + yield waitForCondition(() => !isFullscreenSizeMode()); + ok(!fullscreenButton.checked, "Fullscreen button should not be checked when not in fullscreen.") +}); diff --git a/browser/components/customizableui/test/browser_885530_showInPrivateBrowsing.js b/browser/components/customizableui/test/browser_885530_showInPrivateBrowsing.js new file mode 100644 index 000000000..e55c21862 --- /dev/null +++ b/browser/components/customizableui/test/browser_885530_showInPrivateBrowsing.js @@ -0,0 +1,134 @@ +/* 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 kWidgetId = "some-widget"; + +function assertWidgetExists(aWindow, aExists) { + if (aExists) { + ok(aWindow.document.getElementById(kWidgetId), + "Should have found test widget in the window"); + } else { + is(aWindow.document.getElementById(kWidgetId), null, + "Should not have found test widget in the window"); + } +} + +// A widget that is created with showInPrivateBrowsing undefined should +// have that value default to true. +add_task(function() { + let wrapper = CustomizableUI.createWidget({ + id: kWidgetId + }); + ok(wrapper.showInPrivateBrowsing, + "showInPrivateBrowsing should have defaulted to true."); + CustomizableUI.destroyWidget(kWidgetId); +}); + +// Add a widget via the API with showInPrivateBrowsing set to false +// and ensure it does not appear in pre-existing or newly created +// private windows. +add_task(function*() { + let plain1 = yield openAndLoadWindow(); + let private1 = yield openAndLoadWindow({private: true}); + CustomizableUI.createWidget({ + id: kWidgetId, + removable: true, + showInPrivateBrowsing: false + }); + CustomizableUI.addWidgetToArea(kWidgetId, + CustomizableUI.AREA_NAVBAR); + assertWidgetExists(plain1, true); + assertWidgetExists(private1, false); + + // Now open up some new windows. The widget should exist in the new + // plain window, but not the new private window. + let plain2 = yield openAndLoadWindow(); + let private2 = yield openAndLoadWindow({private: true}); + assertWidgetExists(plain2, true); + assertWidgetExists(private2, false); + + // Try moving the widget around and make sure it doesn't get added + // to the private windows. We'll start by appending it to the tabstrip. + CustomizableUI.addWidgetToArea(kWidgetId, + CustomizableUI.AREA_TABSTRIP); + assertWidgetExists(plain1, true); + assertWidgetExists(plain2, true); + assertWidgetExists(private1, false); + assertWidgetExists(private2, false); + + // And then move it to the beginning of the tabstrip. + CustomizableUI.moveWidgetWithinArea(kWidgetId, 0); + assertWidgetExists(plain1, true); + assertWidgetExists(plain2, true); + assertWidgetExists(private1, false); + assertWidgetExists(private2, false); + + CustomizableUI.removeWidgetFromArea("some-widget"); + assertWidgetExists(plain1, false); + assertWidgetExists(plain2, false); + assertWidgetExists(private1, false); + assertWidgetExists(private2, false); + + yield Promise.all([plain1, plain2, private1, private2].map(promiseWindowClosed)); + + CustomizableUI.destroyWidget("some-widget"); +}); + +// Add a widget via the API with showInPrivateBrowsing set to true, +// and ensure that it appears in pre-existing or newly created +// private browsing windows. +add_task(function*() { + let plain1 = yield openAndLoadWindow(); + let private1 = yield openAndLoadWindow({private: true}); + + CustomizableUI.createWidget({ + id: kWidgetId, + removable: true, + showInPrivateBrowsing: true + }); + CustomizableUI.addWidgetToArea(kWidgetId, + CustomizableUI.AREA_NAVBAR); + assertWidgetExists(plain1, true); + assertWidgetExists(private1, true); + + // Now open up some new windows. The widget should exist in the new + // plain window, but not the new private window. + let plain2 = yield openAndLoadWindow(); + let private2 = yield openAndLoadWindow({private: true}); + + assertWidgetExists(plain2, true); + assertWidgetExists(private2, true); + + // Try moving the widget around and make sure it doesn't get added + // to the private windows. We'll start by appending it to the tabstrip. + CustomizableUI.addWidgetToArea(kWidgetId, + CustomizableUI.AREA_TABSTRIP); + assertWidgetExists(plain1, true); + assertWidgetExists(plain2, true); + assertWidgetExists(private1, true); + assertWidgetExists(private2, true); + + // And then move it to the beginning of the tabstrip. + CustomizableUI.moveWidgetWithinArea(kWidgetId, 0); + assertWidgetExists(plain1, true); + assertWidgetExists(plain2, true); + assertWidgetExists(private1, true); + assertWidgetExists(private2, true); + + CustomizableUI.removeWidgetFromArea("some-widget"); + assertWidgetExists(plain1, false); + assertWidgetExists(plain2, false); + assertWidgetExists(private1, false); + assertWidgetExists(private2, false); + + yield Promise.all([plain1, plain2, private1, private2].map(promiseWindowClosed)); + + CustomizableUI.destroyWidget("some-widget"); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_886323_buildArea_removable_nodes.js b/browser/components/customizableui/test/browser_886323_buildArea_removable_nodes.js new file mode 100644 index 000000000..f46141c4f --- /dev/null +++ b/browser/components/customizableui/test/browser_886323_buildArea_removable_nodes.js @@ -0,0 +1,46 @@ +/* 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 kButtonId = "test-886323-removable-moved-node"; +const kLazyAreaId = "test-886323-lazy-area-for-removability-testing"; + +var gNavBar = document.getElementById(CustomizableUI.AREA_NAVBAR); +var gLazyArea; + +// Removable nodes shouldn't be moved by buildArea +add_task(function*() { + let dummyBtn = createDummyXULButton(kButtonId, "Dummy"); + dummyBtn.setAttribute("removable", "true"); + gNavBar.customizationTarget.appendChild(dummyBtn); + let popupSet = document.getElementById("mainPopupSet"); + gLazyArea = document.createElementNS(kNSXUL, "panel"); + gLazyArea.id = kLazyAreaId; + gLazyArea.setAttribute("hidden", "true"); + popupSet.appendChild(gLazyArea); + CustomizableUI.registerArea(kLazyAreaId, { + type: CustomizableUI.TYPE_MENU_PANEL, + defaultPlacements: [] + }); + CustomizableUI.addWidgetToArea(kButtonId, kLazyAreaId); + assertAreaPlacements(kLazyAreaId, [kButtonId], + "Placements should have changed because widget is removable."); + let btn = document.getElementById(kButtonId); + btn.setAttribute("removable", "false"); + gLazyArea.customizationTarget = gLazyArea; + CustomizableUI.registerToolbarNode(gLazyArea, []); + assertAreaPlacements(kLazyAreaId, [], "Placements should no longer include widget."); + is(btn.parentNode.id, gNavBar.customizationTarget.id, + "Button shouldn't actually have moved as it's not removable"); + btn = document.getElementById(kButtonId); + if (btn) btn.remove(); + CustomizableUI.removeWidgetFromArea(kButtonId); + CustomizableUI.unregisterArea(kLazyAreaId); + gLazyArea.remove(); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_887438_currentset_shim.js b/browser/components/customizableui/test/browser_887438_currentset_shim.js new file mode 100644 index 000000000..a04299819 --- /dev/null +++ b/browser/components/customizableui/test/browser_887438_currentset_shim.js @@ -0,0 +1,75 @@ +/* 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"; + +var navbar = document.getElementById("nav-bar"); +var navbarCT = navbar.customizationTarget; +var overflowPanelList = document.getElementById("widget-overflow-list"); + +// Reading currentset +add_task(function() { + let nodeIds = []; + for (let node of navbarCT.childNodes) { + if (node.getAttribute("skipintoolbarset") != "true") { + nodeIds.push(node.id); + } + } + for (let node of overflowPanelList.childNodes) { + if (node.getAttribute("skipintoolbarset") != "true") { + nodeIds.push(node.id); + } + } + let currentSet = navbar.currentSet; + is(currentSet.split(',').length, nodeIds.length, "Should be just as many nodes as there are."); + is(currentSet, nodeIds.join(','), "Current set and node IDs should match."); +}); + +// Insert, then remove items +add_task(function() { + let currentSet = navbar.currentSet; + let newCurrentSet = currentSet.replace('home-button', 'feed-button,sync-button,home-button'); + navbar.currentSet = newCurrentSet; + is(newCurrentSet, navbar.currentSet, "Current set should match expected current set."); + let feedBtn = document.getElementById("feed-button"); + let syncBtn = document.getElementById("sync-button"); + ok(feedBtn, "Feed button should have been added."); + ok(syncBtn, "Sync button should have been added."); + if (feedBtn && syncBtn) { + let feedParent = feedBtn.parentNode; + let syncParent = syncBtn.parentNode; + ok(feedParent == navbarCT || feedParent == overflowPanelList, + "Feed button should be in navbar or overflow"); + ok(syncParent == navbarCT || syncParent == overflowPanelList, + "Feed button should be in navbar or overflow"); + is(feedBtn.nextElementSibling, syncBtn, "Feed button should be next to sync button."); + let homeBtn = document.getElementById("home-button"); + is(syncBtn.nextElementSibling, homeBtn, "Sync button should be next to home button."); + } + navbar.currentSet = currentSet; + is(currentSet, navbar.currentSet, "Should be able to remove the added items."); +}); + +// Simultaneous insert/remove: +add_task(function() { + let currentSet = navbar.currentSet; + let newCurrentSet = currentSet.replace('home-button', 'feed-button'); + navbar.currentSet = newCurrentSet; + is(newCurrentSet, navbar.currentSet, "Current set should match expected current set."); + let feedBtn = document.getElementById("feed-button"); + ok(feedBtn, "Feed button should have been added."); + let homeBtn = document.getElementById("home-button"); + ok(!homeBtn, "Home button should have been removed."); + if (feedBtn) { + let feedParent = feedBtn.parentNode; + ok(feedParent == navbarCT || feedParent == overflowPanelList, + "Feed button should be in navbar or overflow"); + } + navbar.currentSet = currentSet; + is(currentSet, navbar.currentSet, "Should be able to return to original state."); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_888817_currentset_updating.js b/browser/components/customizableui/test/browser_888817_currentset_updating.js new file mode 100644 index 000000000..6e7c4e95a --- /dev/null +++ b/browser/components/customizableui/test/browser_888817_currentset_updating.js @@ -0,0 +1,57 @@ +/* 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"; + +// Adding, moving and removing items should update the relevant currentset attributes +add_task(function*() { + ok(CustomizableUI.inDefaultState, "Should be in the default state when we start"); + let personalbar = document.getElementById(CustomizableUI.AREA_BOOKMARKS); + setToolbarVisibility(personalbar, true); + ok(!CustomizableUI.inDefaultState, "Making the bookmarks toolbar visible takes it out of the default state"); + + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + personalbar = document.getElementById(CustomizableUI.AREA_BOOKMARKS); + let navbarCurrentset = navbar.getAttribute("currentset") || navbar.currentSet; + let personalbarCurrentset = personalbar.getAttribute("currentset") || personalbar.currentSet; + + let otherWin = yield openAndLoadWindow(); + let otherNavbar = otherWin.document.getElementById(CustomizableUI.AREA_NAVBAR); + let otherPersonalbar = otherWin.document.getElementById(CustomizableUI.AREA_BOOKMARKS); + + CustomizableUI.moveWidgetWithinArea("home-button", 0); + navbarCurrentset = "home-button," + navbarCurrentset.replace(",home-button", ""); + is(navbar.getAttribute("currentset"), navbarCurrentset, + "Should have updated currentSet after move."); + is(otherNavbar.getAttribute("currentset"), navbarCurrentset, + "Should have updated other window's currentSet after move."); + + CustomizableUI.addWidgetToArea("home-button", CustomizableUI.AREA_BOOKMARKS); + navbarCurrentset = navbarCurrentset.replace("home-button,", ""); + personalbarCurrentset = personalbarCurrentset + ",home-button"; + is(navbar.getAttribute("currentset"), navbarCurrentset, + "Should have updated navbar currentSet after implied remove."); + is(otherNavbar.getAttribute("currentset"), navbarCurrentset, + "Should have updated other window's navbar currentSet after implied remove."); + is(personalbar.getAttribute("currentset"), personalbarCurrentset, + "Should have updated personalbar currentSet after add."); + is(otherPersonalbar.getAttribute("currentset"), personalbarCurrentset, + "Should have updated other window's personalbar currentSet after add."); + + CustomizableUI.removeWidgetFromArea("home-button"); + personalbarCurrentset = personalbarCurrentset.replace(",home-button", ""); + is(personalbar.getAttribute("currentset"), personalbarCurrentset, + "Should have updated currentSet after remove."); + is(otherPersonalbar.getAttribute("currentset"), personalbarCurrentset, + "Should have updated other window's currentSet after remove."); + + yield promiseWindowClosed(otherWin); + // Reset in asyncCleanup will put our button back for us. +}); + +add_task(function* asyncCleanup() { + let personalbar = document.getElementById(CustomizableUI.AREA_BOOKMARKS); + setToolbarVisibility(personalbar, false); + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_890140_orphaned_placeholders.js b/browser/components/customizableui/test/browser_890140_orphaned_placeholders.js new file mode 100644 index 000000000..84b126a9b --- /dev/null +++ b/browser/components/customizableui/test/browser_890140_orphaned_placeholders.js @@ -0,0 +1,210 @@ +/* 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"; + +requestLongerTimeout(2); + +// One orphaned item should have two placeholders next to it. +add_task(function*() { + yield startCustomizing(); + + if (isInDevEdition()) { + CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_PANEL); + ok(!CustomizableUI.inDefaultState, "Should no longer be in default state."); + } + if (!isInDevEdition()) { + ok(CustomizableUI.inDefaultState, "Should be in default state."); + } else { + ok(!CustomizableUI.inDefaultState, "Should not be in default state if on DevEdition."); + } + + // This test relies on an exact number of widgets being in the panel. + // Remove the sync-button to satisfy that. (bug 1229236) + CustomizableUI.removeWidgetFromArea("sync-button"); + let panel = document.getElementById(CustomizableUI.AREA_PANEL); + let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL); + + assertAreaPlacements(CustomizableUI.AREA_PANEL, placements); + is(getVisiblePlaceholderCount(panel), 2, "Should only have 2 visible placeholders before exiting"); + + yield endCustomizing(); + yield startCustomizing(); + is(getVisiblePlaceholderCount(panel), 2, "Should only have 2 visible placeholders after re-entering"); + + if (isInDevEdition()) { + CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_NAVBAR, 2); + } + + CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL); + ok(CustomizableUI.inDefaultState, "Should be in default state again."); +}); + +// Two orphaned items should have one placeholder next to them (case 1). +add_task(function*() { + yield startCustomizing(); + + if (isInDevEdition()) { + CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_PANEL); + } + + // This test relies on an exact number of widgets being in the panel. + // Remove the sync-button to satisfy that. (bug 1229236) + CustomizableUI.removeWidgetFromArea("sync-button"); + + let btn = document.getElementById("open-file-button"); + let panel = document.getElementById(CustomizableUI.AREA_PANEL); + let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL); + let placementsAfterAppend = placements; + + placementsAfterAppend = placements.concat(["open-file-button"]); + simulateItemDrag(btn, panel); + + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterAppend); + + ok(!CustomizableUI.inDefaultState, "Should not be in default state."); + + is(getVisiblePlaceholderCount(panel), 1, "Should only have 1 visible placeholder before exiting"); + + yield endCustomizing(); + yield startCustomizing(); + is(getVisiblePlaceholderCount(panel), 1, "Should only have 1 visible placeholder after re-entering"); + + let palette = document.getElementById("customization-palette"); + simulateItemDrag(btn, palette); + + btn = document.getElementById("open-file-button"); + simulateItemDrag(btn, palette); + + if (isInDevEdition()) { + CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_NAVBAR, 2); + } + + CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL); + ok(CustomizableUI.inDefaultState, "Should be in default state again."); +}); + +// Two orphaned items should have one placeholder next to them (case 2). +add_task(function*() { + yield startCustomizing(); + + if (isInDevEdition()) { + CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_PANEL); + } + // This test relies on an exact number of widgets being in the panel. + // Remove the sync-button to satisfy that. (bug 1229236) + CustomizableUI.removeWidgetFromArea("sync-button"); + + let btn = document.getElementById("add-ons-button"); + let btn2 = document.getElementById("developer-button"); + let panel = document.getElementById(CustomizableUI.AREA_PANEL); + let palette = document.getElementById("customization-palette"); + let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL); + + let placementsAfterAppend = placements.filter(p => p != btn.id && p != btn2.id); + simulateItemDrag(btn, palette); + simulateItemDrag(btn2, palette); + + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterAppend); + ok(!CustomizableUI.inDefaultState, "Should no longer be in default state."); + is(getVisiblePlaceholderCount(panel), 1, "Should only have 1 visible placeholder before exiting"); + + yield endCustomizing(); + yield startCustomizing(); + is(getVisiblePlaceholderCount(panel), 1, "Should only have 1 visible placeholder after re-entering"); + + simulateItemDrag(btn, panel); + simulateItemDrag(btn2, panel); + + assertAreaPlacements(CustomizableUI.AREA_PANEL, placements); + + if (isInDevEdition()) { + CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_NAVBAR, 2); + } + + CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL); + ok(CustomizableUI.inDefaultState, "Should be in default state again."); +}); + +// A wide widget at the bottom of the panel should have three placeholders after it. +add_task(function*() { + yield startCustomizing(); + + if (isInDevEdition()) { + CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_PANEL); + } + + // This test relies on an exact number of widgets being in the panel. + // Remove the sync-button to satisfy that. (bug 1229236) + CustomizableUI.removeWidgetFromArea("sync-button"); + + let btn = document.getElementById("edit-controls"); + let btn2 = document.getElementById("developer-button"); + let panel = document.getElementById(CustomizableUI.AREA_PANEL); + let palette = document.getElementById("customization-palette"); + let placements = getAreaWidgetIds(CustomizableUI.AREA_PANEL); + + placements.pop(); + simulateItemDrag(btn2, palette); + + let placementsAfterAppend = placements.concat([placements.shift()]); + simulateItemDrag(btn, panel); + assertAreaPlacements(CustomizableUI.AREA_PANEL, placementsAfterAppend); + ok(!CustomizableUI.inDefaultState, "Should no longer be in default state."); + is(getVisiblePlaceholderCount(panel), 3, "Should have 3 visible placeholders before exiting"); + + yield endCustomizing(); + yield startCustomizing(); + is(getVisiblePlaceholderCount(panel), 3, "Should have 3 visible placeholders after re-entering"); + + simulateItemDrag(btn2, panel); + + let zoomControls = document.getElementById("zoom-controls"); + simulateItemDrag(btn, zoomControls); + + if (isInDevEdition()) { + CustomizableUI.addWidgetToArea("developer-button", CustomizableUI.AREA_NAVBAR, 2); + } + + CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL); + ok(CustomizableUI.inDefaultState, "Should be in default state again."); +}); + +// The default placements should have two placeholders at the bottom (or 1 in win8). +add_task(function*() { + yield startCustomizing(); + let numPlaceholders = -1; + + if (isInDevEdition()) { + numPlaceholders = 3; + } else { + numPlaceholders = 2; + } + + let panel = document.getElementById(CustomizableUI.AREA_PANEL); + ok(CustomizableUI.inDefaultState, "Should be in default state."); + + // This test relies on an exact number of widgets being in the panel. + // Remove the sync-button to satisfy that. (bug 1229236) + CustomizableUI.removeWidgetFromArea("sync-button"); + + is(getVisiblePlaceholderCount(panel), numPlaceholders, "Should have " + numPlaceholders + " visible placeholders before exiting"); + + yield endCustomizing(); + yield startCustomizing(); + is(getVisiblePlaceholderCount(panel), numPlaceholders, "Should have " + numPlaceholders + " visible placeholders after re-entering"); + + CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL); + ok(CustomizableUI.inDefaultState, "Should still be in default state."); +}); + +add_task(function* asyncCleanup() { + yield endCustomizing(); + yield resetCustomization(); +}); + +function getVisiblePlaceholderCount(aPanel) { + let visiblePlaceholders = aPanel.querySelectorAll(".panel-customization-placeholder:not([hidden=true])"); + return visiblePlaceholders.length; +} diff --git a/browser/components/customizableui/test/browser_890262_destroyWidget_after_add_to_panel.js b/browser/components/customizableui/test/browser_890262_destroyWidget_after_add_to_panel.js new file mode 100644 index 000000000..13f2bd7ba --- /dev/null +++ b/browser/components/customizableui/test/browser_890262_destroyWidget_after_add_to_panel.js @@ -0,0 +1,68 @@ +/* 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 kLazyAreaId = "test-890262-lazy-area"; +const kWidget1Id = "test-890262-widget1"; +const kWidget2Id = "test-890262-widget2"; + +setupArea(); + +// Destroying a widget after defaulting it to a non-legacy area should work. +add_task(function() { + CustomizableUI.createWidget({ + id: kWidget1Id, + removable: true, + defaultArea: kLazyAreaId + }); + let noError = true; + try { + CustomizableUI.destroyWidget(kWidget1Id); + } catch (ex) { + Cu.reportError(ex); + noError = false; + } + ok(noError, "Shouldn't throw an exception for a widget that was created in a not-yet-constructed area"); +}); + +// Destroying a widget after moving it to a non-legacy area should work. +add_task(function() { + CustomizableUI.createWidget({ + id: kWidget2Id, + removable: true, + defaultArea: CustomizableUI.AREA_NAVBAR + }); + + CustomizableUI.addWidgetToArea(kWidget2Id, kLazyAreaId); + let noError = true; + try { + CustomizableUI.destroyWidget(kWidget2Id); + } catch (ex) { + Cu.reportError(ex); + noError = false; + } + ok(noError, "Shouldn't throw an exception for a widget that was added to a not-yet-constructed area"); +}); + +add_task(function* asyncCleanup() { + let lazyArea = document.getElementById(kLazyAreaId); + if (lazyArea) { + lazyArea.remove(); + } + try { + CustomizableUI.unregisterArea(kLazyAreaId); + } catch (ex) {} // If we didn't register successfully for some reason + yield resetCustomization(); +}); + +function setupArea() { + let lazyArea = document.createElementNS(kNSXUL, "hbox"); + lazyArea.id = kLazyAreaId; + document.getElementById("nav-bar").appendChild(lazyArea); + CustomizableUI.registerArea(kLazyAreaId, { + type: CustomizableUI.TYPE_TOOLBAR, + defaultPlacements: [] + }); +} diff --git a/browser/components/customizableui/test/browser_892955_isWidgetRemovable_for_removed_widgets.js b/browser/components/customizableui/test/browser_892955_isWidgetRemovable_for_removed_widgets.js new file mode 100644 index 000000000..67ef82b82 --- /dev/null +++ b/browser/components/customizableui/test/browser_892955_isWidgetRemovable_for_removed_widgets.js @@ -0,0 +1,30 @@ +/* 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 kWidgetId = "test-892955-remove-widget"; + +// Removing a destroyed widget should work. +add_task(function*() { + let widgetSpec = { + id: kWidgetId, + defaultArea: CustomizableUI.AREA_NAVBAR + }; + + CustomizableUI.createWidget(widgetSpec); + CustomizableUI.destroyWidget(kWidgetId); + let noError = true; + try { + CustomizableUI.removeWidgetFromArea(kWidgetId); + } catch (ex) { + noError = false; + Cu.reportError(ex); + } + ok(noError, "Shouldn't throw an error removing a destroyed widget."); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_892956_destroyWidget_defaultPlacements.js b/browser/components/customizableui/test/browser_892956_destroyWidget_defaultPlacements.js new file mode 100644 index 000000000..c7047c797 --- /dev/null +++ b/browser/components/customizableui/test/browser_892956_destroyWidget_defaultPlacements.js @@ -0,0 +1,24 @@ +/* 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 kWidgetId = "test-892956-destroyWidget-defaultPlacement"; + +// destroyWidget should clean up defaultPlacements if the widget had a defaultArea +add_task(function*() { + ok(CustomizableUI.inDefaultState, "Should be in the default state when we start"); + + let widgetSpec = { + id: kWidgetId, + defaultArea: CustomizableUI.AREA_NAVBAR + }; + CustomizableUI.createWidget(widgetSpec); + CustomizableUI.destroyWidget(kWidgetId); + ok(CustomizableUI.inDefaultState, "Should be in the default state when we finish"); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js b/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js new file mode 100644 index 000000000..3bc449add --- /dev/null +++ b/browser/components/customizableui/test/browser_901207_searchbar_in_panel.js @@ -0,0 +1,113 @@ +/* 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"; + +logActiveElement(); + +function* waitForSearchBarFocus() +{ + let searchbar = document.getElementById("searchbar"); + yield waitForCondition(function () { + logActiveElement(); + return document.activeElement === searchbar.textbox.inputField; + }); +} + +// Ctrl+K should open the menu panel and focus the search bar if the search bar is in the panel. +add_task(function*() { + let searchbar = document.getElementById("searchbar"); + gCustomizeMode.addToPanel(searchbar); + let placement = CustomizableUI.getPlacementOfWidget("search-container"); + is(placement.area, CustomizableUI.AREA_PANEL, "Should be in panel"); + + let shownPanelPromise = promisePanelShown(window); + sendWebSearchKeyCommand(); + yield shownPanelPromise; + + yield waitForSearchBarFocus(); + + let hiddenPanelPromise = promisePanelHidden(window); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield hiddenPanelPromise; + CustomizableUI.reset(); +}); + +// Ctrl+K should give focus to the searchbar when the searchbar is in the menupanel and the panel is already opened. +add_task(function*() { + let searchbar = document.getElementById("searchbar"); + gCustomizeMode.addToPanel(searchbar); + let placement = CustomizableUI.getPlacementOfWidget("search-container"); + is(placement.area, CustomizableUI.AREA_PANEL, "Should be in panel"); + + let shownPanelPromise = promisePanelShown(window); + PanelUI.toggle({type: "command"}); + yield shownPanelPromise; + + sendWebSearchKeyCommand(); + + yield waitForSearchBarFocus(); + + let hiddenPanelPromise = promisePanelHidden(window); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield hiddenPanelPromise; + CustomizableUI.reset(); +}); + +// Ctrl+K should open the overflow panel and focus the search bar if the search bar is overflowed. +add_task(function*() { + this.originalWindowWidth = window.outerWidth; + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar."); + ok(CustomizableUI.inDefaultState, "Should start in default state."); + + window.resizeTo(360, window.outerHeight); + yield waitForCondition(() => navbar.getAttribute("overflowing") == "true"); + ok(!navbar.querySelector("#search-container"), "Search container should be overflowing"); + + let shownPanelPromise = promiseOverflowShown(window); + sendWebSearchKeyCommand(); + yield shownPanelPromise; + + let chevron = document.getElementById("nav-bar-overflow-button"); + yield waitForCondition(() => chevron.open); + + yield waitForSearchBarFocus(); + + let hiddenPanelPromise = promiseOverflowHidden(window); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield hiddenPanelPromise; + navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + window.resizeTo(this.originalWindowWidth, window.outerHeight); + yield waitForCondition(() => !navbar.hasAttribute("overflowing")); + ok(!navbar.hasAttribute("overflowing"), "Should not have an overflowing toolbar."); +}); + +// Ctrl+K should focus the search bar if it is in the navbar and not overflowing. +add_task(function*() { + let placement = CustomizableUI.getPlacementOfWidget("search-container"); + is(placement.area, CustomizableUI.AREA_NAVBAR, "Should be in nav-bar"); + + sendWebSearchKeyCommand(); + + yield waitForSearchBarFocus(); +}); + + +function sendWebSearchKeyCommand() { + if (Services.appinfo.OS === "Darwin") + EventUtils.synthesizeKey("k", { accelKey: true }); + else + EventUtils.synthesizeKey("k", { ctrlKey: true }); +} + +function logActiveElement() { + let element = document.activeElement; + let str = ""; + while (element && element.parentNode) { + str = " (" + element.localName + "#" + element.id + "." + [...element.classList].join(".") + ") >" + str; + element = element.parentNode; + } + info("Active element: " + element ? str : "null"); +} diff --git a/browser/components/customizableui/test/browser_909779_overflow_toolbars_new_window.js b/browser/components/customizableui/test/browser_909779_overflow_toolbars_new_window.js new file mode 100644 index 000000000..f39d13ff4 --- /dev/null +++ b/browser/components/customizableui/test/browser_909779_overflow_toolbars_new_window.js @@ -0,0 +1,31 @@ +/* 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"; + +// Resize to a small window, open a new window, check that new window handles overflow properly +add_task(function*() { + let originalWindowWidth = window.outerWidth; + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar."); + let oldChildCount = navbar.customizationTarget.childElementCount; + window.resizeTo(400, window.outerHeight); + yield waitForCondition(() => navbar.hasAttribute("overflowing")); + ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + + ok(navbar.customizationTarget.childElementCount < oldChildCount, "Should have fewer children."); + let newWindow = yield openAndLoadWindow(); + let otherNavBar = newWindow.document.getElementById(CustomizableUI.AREA_NAVBAR); + yield waitForCondition(() => otherNavBar.hasAttribute("overflowing")); + ok(otherNavBar.hasAttribute("overflowing"), "Other window should have an overflowing toolbar."); + yield promiseWindowClosed(newWindow); + + window.resizeTo(originalWindowWidth, window.outerHeight); + yield waitForCondition(() => !navbar.hasAttribute("overflowing")); + ok(!navbar.hasAttribute("overflowing"), "Should no longer have an overflowing toolbar."); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_913972_currentset_overflow.js b/browser/components/customizableui/test/browser_913972_currentset_overflow.js new file mode 100644 index 000000000..7d754d79b --- /dev/null +++ b/browser/components/customizableui/test/browser_913972_currentset_overflow.js @@ -0,0 +1,55 @@ +/* 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"; + +var navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + +// Resize to a small window, resize back, shouldn't affect currentSet +add_task(function*() { + let originalWindowWidth = window.outerWidth; + let oldCurrentSet = navbar.currentSet; + ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar."); + ok(CustomizableUI.inDefaultState, "Should start in default state."); + let oldChildCount = navbar.customizationTarget.childElementCount; + window.resizeTo(400, window.outerHeight); + yield waitForCondition(() => navbar.hasAttribute("overflowing")); + ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + is(navbar.currentSet, oldCurrentSet, "Currentset should be the same when overflowing."); + ok(CustomizableUI.inDefaultState, "Should still be in default state when overflowing."); + ok(navbar.customizationTarget.childElementCount < oldChildCount, "Should have fewer children."); + window.resizeTo(originalWindowWidth, window.outerHeight); + yield waitForCondition(() => !navbar.hasAttribute("overflowing")); + ok(!navbar.hasAttribute("overflowing"), "Should no longer have an overflowing toolbar."); + is(navbar.currentSet, oldCurrentSet, "Currentset should still be the same now we're no longer overflowing."); + ok(CustomizableUI.inDefaultState, "Should still be in default state now we're no longer overflowing."); + + // Verify actual physical placements match those of the placement array: + let placementCounter = 0; + let placements = CustomizableUI.getWidgetIdsInArea(CustomizableUI.AREA_NAVBAR); + for (let node of navbar.customizationTarget.childNodes) { + if (node.getAttribute("skipintoolbarset") == "true") { + continue; + } + is(placements[placementCounter++], node.id, "Nodes should match after overflow"); + } + is(placements.length, placementCounter, "Should have as many nodes as expected"); + is(navbar.customizationTarget.childElementCount, oldChildCount, "Number of nodes should match"); +}); + +// Enter and exit customization mode, check that currentSet works +add_task(function*() { + let oldCurrentSet = navbar.currentSet; + ok(CustomizableUI.inDefaultState, "Should start in default state."); + yield startCustomizing(); + ok(CustomizableUI.inDefaultState, "Should be in default state in customization mode."); + is(navbar.currentSet, oldCurrentSet, "Currentset should be the same in customization mode."); + yield endCustomizing(); + ok(CustomizableUI.inDefaultState, "Should be in default state after customization mode."); + is(navbar.currentSet, oldCurrentSet, "Currentset should be the same after customization mode."); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_914138_widget_API_overflowable_toolbar.js b/browser/components/customizableui/test/browser_914138_widget_API_overflowable_toolbar.js new file mode 100644 index 000000000..35ba79bec --- /dev/null +++ b/browser/components/customizableui/test/browser_914138_widget_API_overflowable_toolbar.js @@ -0,0 +1,131 @@ +/* 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"; + +var navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); +var overflowList = document.getElementById(navbar.getAttribute("overflowtarget")); + +const kTestBtn1 = "test-addWidgetToArea-overflow"; +const kTestBtn2 = "test-removeWidgetFromArea-overflow"; +const kTestBtn3 = "test-createWidget-overflow"; +const kHomeBtn = "home-button"; +const kDownloadsBtn = "downloads-button"; +const kSearchBox = "search-container"; +const kStarBtn = "bookmarks-menu-button"; + +var originalWindowWidth; + +// Adding a widget should add it next to the widget it's being inserted next to. +add_task(function*() { + originalWindowWidth = window.outerWidth; + createDummyXULButton(kTestBtn1, "Test"); + ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar."); + ok(CustomizableUI.inDefaultState, "Should start in default state."); + + window.resizeTo(400, window.outerHeight); + yield waitForCondition(() => navbar.hasAttribute("overflowing")); + ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + ok(!navbar.querySelector("#" + kHomeBtn), "Home button should no longer be in the navbar"); + let homeBtnNode = overflowList.querySelector("#" + kHomeBtn); + ok(homeBtnNode, "Home button should be overflowing"); + ok(homeBtnNode && homeBtnNode.getAttribute("overflowedItem") == "true", "Home button should have overflowedItem attribute"); + + let placementOfHomeButton = CustomizableUI.getWidgetIdsInArea(navbar.id).indexOf(kHomeBtn); + CustomizableUI.addWidgetToArea(kTestBtn1, navbar.id, placementOfHomeButton); + ok(!navbar.querySelector("#" + kTestBtn1), "New button should not be in the navbar"); + let newButtonNode = overflowList.querySelector("#" + kTestBtn1); + ok(newButtonNode, "New button should be overflowing"); + ok(newButtonNode && newButtonNode.getAttribute("overflowedItem") == "true", "New button should have overflowedItem attribute"); + let nextEl = newButtonNode && newButtonNode.nextSibling; + is(nextEl && nextEl.id, kHomeBtn, "Test button should be next to home button."); + + window.resizeTo(originalWindowWidth, window.outerHeight); + yield waitForCondition(() => !navbar.hasAttribute("overflowing")); + ok(!navbar.hasAttribute("overflowing"), "Should not have an overflowing toolbar."); + ok(navbar.querySelector("#" + kHomeBtn), "Home button should be in the navbar"); + ok(homeBtnNode && (homeBtnNode.getAttribute("overflowedItem") != "true"), "Home button should no longer have overflowedItem attribute"); + ok(!overflowList.querySelector("#" + kHomeBtn), "Home button should no longer be overflowing"); + ok(navbar.querySelector("#" + kTestBtn1), "Test button should be in the navbar"); + ok(!overflowList.querySelector("#" + kTestBtn1), "Test button should no longer be overflowing"); + ok(newButtonNode && (newButtonNode.getAttribute("overflowedItem") != "true"), "New button should no longer have overflowedItem attribute"); + let el = document.getElementById(kTestBtn1); + if (el) { + CustomizableUI.removeWidgetFromArea(kTestBtn1); + el.remove(); + } + window.resizeTo(originalWindowWidth, window.outerHeight); +}); + +// Removing a widget should remove it from the overflow list if that is where it is, and update it accordingly. +add_task(function*() { + createDummyXULButton(kTestBtn2, "Test"); + ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar."); + ok(CustomizableUI.inDefaultState, "Should start in default state."); + CustomizableUI.addWidgetToArea(kTestBtn2, navbar.id); + ok(!navbar.hasAttribute("overflowing"), "Should still have a non-overflowing toolbar."); + + window.resizeTo(400, window.outerHeight); + yield waitForCondition(() => navbar.hasAttribute("overflowing")); + ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + ok(!navbar.querySelector("#" + kTestBtn2), "Test button should not be in the navbar"); + ok(overflowList.querySelector("#" + kTestBtn2), "Test button should be overflowing"); + + CustomizableUI.removeWidgetFromArea(kTestBtn2); + + ok(!overflowList.querySelector("#" + kTestBtn2), "Test button should not be overflowing."); + ok(!navbar.querySelector("#" + kTestBtn2), "Test button should not be in the navbar"); + ok(gNavToolbox.palette.querySelector("#" + kTestBtn2), "Test button should be in the palette"); + + window.resizeTo(originalWindowWidth, window.outerHeight); + yield waitForCondition(() => !navbar.hasAttribute("overflowing")); + ok(!navbar.hasAttribute("overflowing"), "Should not have an overflowing toolbar."); + let el = document.getElementById(kTestBtn2); + if (el) { + CustomizableUI.removeWidgetFromArea(kTestBtn2); + el.remove(); + } + window.resizeTo(originalWindowWidth, window.outerHeight); +}); + +// Constructing a widget while overflown should set the right class on it. +add_task(function*() { + originalWindowWidth = window.outerWidth; + ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar."); + ok(CustomizableUI.inDefaultState, "Should start in default state."); + + window.resizeTo(400, window.outerHeight); + yield waitForCondition(() => navbar.hasAttribute("overflowing")); + ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + ok(!navbar.querySelector("#" + kHomeBtn), "Home button should no longer be in the navbar"); + let homeBtnNode = overflowList.querySelector("#" + kHomeBtn); + ok(homeBtnNode, "Home button should be overflowing"); + ok(homeBtnNode && homeBtnNode.getAttribute("overflowedItem") == "true", "Home button should have overflowedItem class"); + + let testBtnSpec = {id: kTestBtn3, label: "Overflowable widget test", defaultArea: "nav-bar"}; + CustomizableUI.createWidget(testBtnSpec); + let testNode = overflowList.querySelector("#" + kTestBtn3); + ok(testNode, "Test button should be overflowing"); + ok(testNode && testNode.getAttribute("overflowedItem") == "true", "Test button should have overflowedItem class"); + + CustomizableUI.destroyWidget(kTestBtn3); + testNode = document.getElementById(kTestBtn3); + ok(!testNode, "Test button should be gone"); + + CustomizableUI.createWidget(testBtnSpec); + testNode = overflowList.querySelector("#" + kTestBtn3); + ok(testNode, "Test button should be overflowing"); + ok(testNode && testNode.getAttribute("overflowedItem") == "true", "Test button should have overflowedItem class"); + + CustomizableUI.removeWidgetFromArea(kTestBtn3); + testNode = document.getElementById(kTestBtn3); + ok(!testNode, "Test button should be gone"); + CustomizableUI.destroyWidget(kTestBtn3); + window.resizeTo(originalWindowWidth, window.outerHeight); +}); + +add_task(function* asyncCleanup() { + window.resizeTo(originalWindowWidth, window.outerHeight); + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_914863_disabled_help_quit_buttons.js b/browser/components/customizableui/test/browser_914863_disabled_help_quit_buttons.js new file mode 100644 index 000000000..b5757eabb --- /dev/null +++ b/browser/components/customizableui/test/browser_914863_disabled_help_quit_buttons.js @@ -0,0 +1,16 @@ +/* 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/. */ + +// Entering then exiting customization mode should reenable the Help and Exit buttons. +add_task(function*() { + yield startCustomizing(); + let helpButton = document.getElementById("PanelUI-help"); + let quitButton = document.getElementById("PanelUI-quit"); + ok(helpButton.getAttribute("disabled") == "true", "Help button should be disabled while in customization mode."); + ok(quitButton.getAttribute("disabled") == "true", "Quit button should be disabled while in customization mode."); + yield endCustomizing(); + + ok(!helpButton.hasAttribute("disabled"), "Help button should not be disabled."); + ok(!quitButton.hasAttribute("disabled"), "Quit button should not be disabled."); +}); diff --git a/browser/components/customizableui/test/browser_918049_skipintoolbarset_dnd.js b/browser/components/customizableui/test/browser_918049_skipintoolbarset_dnd.js new file mode 100644 index 000000000..dffe388dc --- /dev/null +++ b/browser/components/customizableui/test/browser_918049_skipintoolbarset_dnd.js @@ -0,0 +1,38 @@ +/* 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"; + +var navbar; +var skippedItem; + +// Attempting to drag a skipintoolbarset item should work. +add_task(function*() { + navbar = document.getElementById("nav-bar"); + skippedItem = document.createElement("toolbarbutton"); + skippedItem.id = "test-skipintoolbarset-item"; + skippedItem.setAttribute("label", "Test"); + skippedItem.setAttribute("skipintoolbarset", "true"); + skippedItem.setAttribute("removable", "true"); + navbar.customizationTarget.appendChild(skippedItem); + let downloadsButton = document.getElementById("downloads-button"); + yield startCustomizing(); + ok(CustomizableUI.inDefaultState, "Should still be in default state"); + simulateItemDrag(skippedItem, downloadsButton); + ok(CustomizableUI.inDefaultState, "Should still be in default state"); + let skippedItemWrapper = skippedItem.parentNode; + is(skippedItemWrapper.nextSibling && skippedItemWrapper.nextSibling.id, + downloadsButton.parentNode.id, "Should be next to downloads button"); + simulateItemDrag(downloadsButton, skippedItem); + let downloadWrapper = downloadsButton.parentNode; + is(downloadWrapper.nextSibling && downloadWrapper.nextSibling.id, + skippedItem.parentNode.id, "Should be next to skipintoolbarset item"); + ok(CustomizableUI.inDefaultState, "Should still be in default state"); +}); + +add_task(function* asyncCleanup() { + yield endCustomizing(); + skippedItem.remove(); + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_923857_customize_mode_event_wrapping_during_reset.js b/browser/components/customizableui/test/browser_923857_customize_mode_event_wrapping_during_reset.js new file mode 100644 index 000000000..87aca51eb --- /dev/null +++ b/browser/components/customizableui/test/browser_923857_customize_mode_event_wrapping_during_reset.js @@ -0,0 +1,24 @@ +/* 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"; + +// Customize mode reset button should revert correctly +add_task(function*() { + yield startCustomizing(); + let devButton = document.getElementById("developer-button"); + let downloadsButton = document.getElementById("downloads-button"); + let searchBox = document.getElementById("search-container"); + let palette = document.getElementById("customization-palette"); + ok(devButton && downloadsButton && searchBox && palette, "Stuff should exist"); + simulateItemDrag(devButton, downloadsButton); + simulateItemDrag(searchBox, palette); + yield gCustomizeMode.reset(); + ok(CustomizableUI.inDefaultState, "Should be back in default state"); + yield endCustomizing(); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_927717_customize_drag_empty_toolbar.js b/browser/components/customizableui/test/browser_927717_customize_drag_empty_toolbar.js new file mode 100644 index 000000000..d79f6e364 --- /dev/null +++ b/browser/components/customizableui/test/browser_927717_customize_drag_empty_toolbar.js @@ -0,0 +1,26 @@ +/* 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 kTestToolbarId = "test-empty-drag"; + +// Attempting to drag an item to an empty container should work. +add_task(function*() { + yield createToolbarWithPlacements(kTestToolbarId, []); + yield startCustomizing(); + let downloadButton = document.getElementById("downloads-button"); + let customToolbar = document.getElementById(kTestToolbarId); + simulateItemDrag(downloadButton, customToolbar); + assertAreaPlacements(kTestToolbarId, ["downloads-button"]); + ok(downloadButton.parentNode && downloadButton.parentNode.parentNode == customToolbar, + "Button should really be in toolbar"); + yield endCustomizing(); + removeCustomToolbars(); +}); + +add_task(function* asyncCleanup() { + yield endCustomizing(); + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_932928_show_notice_when_palette_empty.js b/browser/components/customizableui/test/browser_932928_show_notice_when_palette_empty.js new file mode 100644 index 000000000..3cbf6be42 --- /dev/null +++ b/browser/components/customizableui/test/browser_932928_show_notice_when_palette_empty.js @@ -0,0 +1,35 @@ +/* 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"; + +// There should be an advert to get more addons when the palette is empty. +add_task(function*() { + yield startCustomizing(); + let visiblePalette = document.getElementById("customization-palette"); + let emptyPaletteNotice = document.getElementById("customization-empty"); + is(emptyPaletteNotice.hidden, true, "The empty palette notice should not be shown when there are items in the palette."); + + while (visiblePalette.childElementCount) { + gCustomizeMode.addToToolbar(visiblePalette.children[0]); + } + is(visiblePalette.childElementCount, 0, "There shouldn't be any items remaining in the visible palette."); + is(emptyPaletteNotice.hidden, false, "The empty palette notice should be shown when there are no items in the palette."); + + yield endCustomizing(); + yield startCustomizing(); + visiblePalette = document.getElementById("customization-palette"); + emptyPaletteNotice = document.getElementById("customization-empty"); + is(emptyPaletteNotice.hidden, false, + "The empty palette notice should be shown when there are no items in the palette and cust. mode is re-entered."); + + gCustomizeMode.removeFromArea(document.getElementById("wrapper-home-button")); + is(emptyPaletteNotice.hidden, true, + "The empty palette notice should not be shown when there is at least one item in the palette."); +}); + +add_task(function* asyncCleanup() { + yield endCustomizing(); + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_934113_menubar_removable.js b/browser/components/customizableui/test/browser_934113_menubar_removable.js new file mode 100644 index 000000000..1d788bced --- /dev/null +++ b/browser/components/customizableui/test/browser_934113_menubar_removable.js @@ -0,0 +1,30 @@ +/* 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"; + +// Attempting to drag the menubar to the navbar shouldn't work. +add_task(function*() { + yield startCustomizing(); + let menuItems = document.getElementById("menubar-items"); + let navbar = document.getElementById("nav-bar"); + let menubar = document.getElementById("toolbar-menubar"); + // Force the menu to be shown. + const kAutohide = menubar.getAttribute("autohide"); + menubar.setAttribute("autohide", "false"); + simulateItemDrag(menuItems, navbar.customizationTarget); + + is(getAreaWidgetIds("nav-bar").indexOf("menubar-items"), -1, "Menu bar shouldn't be in the navbar."); + ok(!navbar.querySelector("#menubar-items"), "Shouldn't find menubar items in the navbar."); + ok(menubar.querySelector("#menubar-items"), "Should find menubar items in the menubar."); + isnot(getAreaWidgetIds("toolbar-menubar").indexOf("menubar-items"), -1, + "Menubar items shouldn't be missing from the navbar."); + menubar.setAttribute("autohide", kAutohide); + yield endCustomizing(); +}); + +add_task(function* asyncCleanup() { + yield endCustomizing(); + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_934951_zoom_in_toolbar.js b/browser/components/customizableui/test/browser_934951_zoom_in_toolbar.js new file mode 100644 index 000000000..dcc183051 --- /dev/null +++ b/browser/components/customizableui/test/browser_934951_zoom_in_toolbar.js @@ -0,0 +1,89 @@ +/* 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 kTimeoutInMS = 20000; + +// Bug 934951 - Zoom controls percentage label doesn't update when it's in the toolbar and you navigate. +add_task(function*() { + CustomizableUI.addWidgetToArea("zoom-controls", CustomizableUI.AREA_NAVBAR); + let tab1 = gBrowser.addTab("about:mozilla"); + yield BrowserTestUtils.browserLoaded(tab1.linkedBrowser); + let tab2 = gBrowser.addTab("about:robots"); + yield BrowserTestUtils.browserLoaded(tab2.linkedBrowser); + gBrowser.selectedTab = tab1; + let zoomResetButton = document.getElementById("zoom-reset-button"); + + registerCleanupFunction(() => { + info("Cleaning up."); + CustomizableUI.reset(); + gBrowser.removeTab(tab2); + gBrowser.removeTab(tab1); + }); + + is(parseInt(zoomResetButton.label, 10), 100, "Default zoom is 100% for about:mozilla"); + let zoomChangePromise = promiseObserverNotification("browser-fullZoom:zoomChange"); + FullZoom.enlarge(); + yield zoomChangePromise; + is(parseInt(zoomResetButton.label, 10), 110, "Zoom is changed to 110% for about:mozilla"); + + let tabSelectPromise = promiseTabSelect(); + gBrowser.selectedTab = tab2; + yield tabSelectPromise; + is(parseInt(zoomResetButton.label, 10), 100, "Default zoom is 100% for about:robots"); + + gBrowser.selectedTab = tab1; + let zoomResetPromise = promiseObserverNotification("browser-fullZoom:zoomReset"); + FullZoom.reset(); + yield zoomResetPromise; + is(parseInt(zoomResetButton.label, 10), 100, "Default zoom is 100% for about:mozilla"); + + // Test zoom label updates while navigating pages in the same tab. + FullZoom.enlarge(); + yield zoomChangePromise; + is(parseInt(zoomResetButton.label, 10), 110, "Zoom is changed to 110% for about:mozilla"); + let attributeChangePromise = promiseAttributeMutation(zoomResetButton, "label", (v) => { + return parseInt(v, 10) == 100; + }); + yield promiseTabLoadEvent(tab1, "about:home"); + yield attributeChangePromise; + is(parseInt(zoomResetButton.label, 10), 100, "Default zoom is 100% for about:home"); + yield promiseTabHistoryNavigation(-1, function() { + return parseInt(zoomResetButton.label, 10) == 110; + }); + is(parseInt(zoomResetButton.label, 10), 110, "Zoom is still 110% for about:mozilla"); + FullZoom.reset(); +}); + +function promiseObserverNotification(aObserver) { + let deferred = Promise.defer(); + function notificationCallback(e) { + Services.obs.removeObserver(notificationCallback, aObserver, false); + clearTimeout(timeoutId); + deferred.resolve(); + } + let timeoutId = setTimeout(() => { + Services.obs.removeObserver(notificationCallback, aObserver, false); + deferred.reject("Notification '" + aObserver + "' did not happen within 20 seconds."); + }, kTimeoutInMS); + Services.obs.addObserver(notificationCallback, aObserver, false); + return deferred.promise; +} + +function promiseTabSelect() { + let deferred = Promise.defer(); + let container = window.gBrowser.tabContainer; + let timeoutId = setTimeout(() => { + container.removeEventListener("TabSelect", callback); + deferred.reject("TabSelect did not happen within 20 seconds"); + }, kTimeoutInMS); + function callback(e) { + container.removeEventListener("TabSelect", callback); + clearTimeout(timeoutId); + executeSoon(deferred.resolve); + } + container.addEventListener("TabSelect", callback); + return deferred.promise; +} diff --git a/browser/components/customizableui/test/browser_938980_navbar_collapsed.js b/browser/components/customizableui/test/browser_938980_navbar_collapsed.js new file mode 100644 index 000000000..fc7fa1a0a --- /dev/null +++ b/browser/components/customizableui/test/browser_938980_navbar_collapsed.js @@ -0,0 +1,121 @@ +/* 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"; + +requestLongerTimeout(2); + +var bookmarksToolbar = document.getElementById("PersonalToolbar"); +var navbar = document.getElementById("nav-bar"); +var tabsToolbar = document.getElementById("TabsToolbar"); + +// Customization reset should restore visibility to default-visible toolbars. +add_task(function*() { + is(navbar.collapsed, false, "Test should start with navbar visible"); + setToolbarVisibility(navbar, false); + is(navbar.collapsed, true, "navbar should be hidden now"); + + yield resetCustomization(); + + is(navbar.collapsed, false, "Customization reset should restore visibility to the navbar"); +}); + +// Customization reset should restore collapsed-state to default-collapsed toolbars. +add_task(function*() { + ok(CustomizableUI.inDefaultState, "Everything should be in its default state"); + + is(bookmarksToolbar.collapsed, true, "Test should start with bookmarks toolbar collapsed"); + ok(bookmarksToolbar.collapsed, "bookmarksToolbar should be collapsed"); + ok(!tabsToolbar.collapsed, "TabsToolbar should not be collapsed"); + is(navbar.collapsed, false, "The nav-bar should be shown by default"); + + setToolbarVisibility(bookmarksToolbar, true); + setToolbarVisibility(navbar, false); + ok(!bookmarksToolbar.collapsed, "bookmarksToolbar should be visible now"); + ok(navbar.collapsed, "navbar should be collapsed"); + is(CustomizableUI.inDefaultState, false, "Should no longer be in default state"); + + yield startCustomizing(); + yield gCustomizeMode.reset(); + yield endCustomizing(); + + is(bookmarksToolbar.collapsed, true, "Customization reset should restore collapsed-state to the bookmarks toolbar"); + ok(!tabsToolbar.collapsed, "TabsToolbar should not be collapsed"); + ok(bookmarksToolbar.collapsed, "The bookmarksToolbar should be collapsed after reset"); + ok(CustomizableUI.inDefaultState, "Everything should be back to default state"); +}); + +// Check that the menubar will be collapsed by resetting, if the platform supports it. +add_task(function*() { + let menubar = document.getElementById("toolbar-menubar"); + const canMenubarCollapse = CustomizableUI.isToolbarDefaultCollapsed(menubar.id); + if (!canMenubarCollapse) { + return; + } + ok(CustomizableUI.inDefaultState, "Everything should be in its default state"); + + is(menubar.getBoundingClientRect().height, 0, "menubar should be hidden by default"); + setToolbarVisibility(menubar, true); + isnot(menubar.getBoundingClientRect().height, 0, "menubar should be visible now"); + + yield startCustomizing(); + yield gCustomizeMode.reset(); + + is(menubar.getAttribute("autohide"), "true", "The menubar should have autohide=true after reset in customization mode"); + is(menubar.getBoundingClientRect().height, 0, "The menubar should have height=0 after reset in customization mode"); + + yield endCustomizing(); + + is(menubar.getAttribute("autohide"), "true", "The menubar should have autohide=true after reset"); + is(menubar.getBoundingClientRect().height, 0, "The menubar should have height=0 after reset"); +}); + +// Customization reset should restore collapsed-state to default-collapsed toolbars. +add_task(function*() { + ok(CustomizableUI.inDefaultState, "Everything should be in its default state"); + ok(bookmarksToolbar.collapsed, "bookmarksToolbar should be collapsed"); + ok(!tabsToolbar.collapsed, "TabsToolbar should not be collapsed"); + + setToolbarVisibility(bookmarksToolbar, true); + ok(!bookmarksToolbar.collapsed, "bookmarksToolbar should be visible now"); + is(CustomizableUI.inDefaultState, false, "Should no longer be in default state"); + + yield startCustomizing(); + + ok(!bookmarksToolbar.collapsed, "The bookmarksToolbar should be visible before reset"); + ok(!navbar.collapsed, "The navbar should be visible before reset"); + ok(!tabsToolbar.collapsed, "TabsToolbar should not be collapsed"); + + yield gCustomizeMode.reset(); + + ok(bookmarksToolbar.collapsed, "The bookmarksToolbar should be collapsed after reset"); + ok(!tabsToolbar.collapsed, "TabsToolbar should not be collapsed"); + ok(!navbar.collapsed, "The navbar should still be visible after reset"); + ok(CustomizableUI.inDefaultState, "Everything should be back to default state"); + yield endCustomizing(); +}); + +// Check that the menubar will be collapsed by resetting, if the platform supports it. +add_task(function*() { + let menubar = document.getElementById("toolbar-menubar"); + const canMenubarCollapse = CustomizableUI.isToolbarDefaultCollapsed(menubar.id); + if (!canMenubarCollapse) { + return; + } + ok(CustomizableUI.inDefaultState, "Everything should be in its default state"); + yield startCustomizing(); + let resetButton = document.getElementById("customization-reset-button"); + is(resetButton.disabled, true, "The reset button should be disabled when in default state"); + + setToolbarVisibility(menubar, true); + is(resetButton.disabled, false, "The reset button should be enabled when not in default state") + ok(!CustomizableUI.inDefaultState, "No longer in default state when the menubar is shown"); + + yield gCustomizeMode.reset(); + + is(resetButton.disabled, true, "The reset button should be disabled when in default state"); + ok(CustomizableUI.inDefaultState, "Everything should be in its default state"); + + yield endCustomizing(); +}); diff --git a/browser/components/customizableui/test/browser_938995_indefaultstate_nonremovable.js b/browser/components/customizableui/test/browser_938995_indefaultstate_nonremovable.js new file mode 100644 index 000000000..1f06c1aac --- /dev/null +++ b/browser/components/customizableui/test/browser_938995_indefaultstate_nonremovable.js @@ -0,0 +1,25 @@ +/* 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 kWidgetId = "test-non-removable-widget"; + +// Adding non-removable items to a toolbar or the panel shouldn't change inDefaultState +add_task(function() { + ok(CustomizableUI.inDefaultState, "Should start in default state"); + + let button = createDummyXULButton(kWidgetId, "Test non-removable inDefaultState handling"); + CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_NAVBAR); + button.setAttribute("removable", "false"); + ok(CustomizableUI.inDefaultState, "Should still be in default state after navbar addition"); + button.remove(); + + button = createDummyXULButton(kWidgetId, "Test non-removable inDefaultState handling"); + CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_PANEL); + button.setAttribute("removable", "false"); + ok(CustomizableUI.inDefaultState, "Should still be in default state after panel addition"); + button.remove(); + ok(CustomizableUI.inDefaultState, "Should be in default state after destroying both widgets"); +}); diff --git a/browser/components/customizableui/test/browser_940013_registerToolbarNode_calls_registerArea.js b/browser/components/customizableui/test/browser_940013_registerToolbarNode_calls_registerArea.js new file mode 100644 index 000000000..c554bffab --- /dev/null +++ b/browser/components/customizableui/test/browser_940013_registerToolbarNode_calls_registerArea.js @@ -0,0 +1,70 @@ +/* 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 kToolbarId = "test-registerToolbarNode-toolbar"; +const kButtonId = "test-registerToolbarNode-button"; +registerCleanupFunction(cleanup); + +// Registering a toolbar with defaultset attribute should work +add_task(function*() { + ok(CustomizableUI.inDefaultState, "Everything should be in its default state."); + let btn = createDummyXULButton(kButtonId); + let toolbar = document.createElement("toolbar"); + toolbar.id = kToolbarId; + toolbar.setAttribute("customizable", true); + toolbar.setAttribute("defaultset", kButtonId); + gNavToolbox.appendChild(toolbar); + ok(CustomizableUI.areas.indexOf(kToolbarId) != -1, + "Toolbar should have been registered automatically."); + is(CustomizableUI.getAreaType(kToolbarId), CustomizableUI.TYPE_TOOLBAR, + "Area should be registered as toolbar"); + assertAreaPlacements(kToolbarId, [kButtonId]); + ok(!CustomizableUI.inDefaultState, "No longer in default state after toolbar is registered and visible."); + CustomizableUI.unregisterArea(kToolbarId, true); + toolbar.remove(); + ok(CustomizableUI.inDefaultState, "Everything should be in its default state."); + btn.remove(); +}); + +// Registering a toolbar without a defaultset attribute should +// wait for the registerArea call +add_task(function*() { + ok(CustomizableUI.inDefaultState, "Everything should be in its default state."); + let btn = createDummyXULButton(kButtonId); + let toolbar = document.createElement("toolbar"); + toolbar.id = kToolbarId; + toolbar.setAttribute("customizable", true); + gNavToolbox.appendChild(toolbar); + ok(CustomizableUI.areas.indexOf(kToolbarId) == -1, + "Toolbar should not yet have been registered automatically."); + CustomizableUI.registerArea(kToolbarId, {defaultPlacements: [kButtonId]}); + ok(CustomizableUI.areas.indexOf(kToolbarId) != -1, + "Toolbar should have been registered now."); + is(CustomizableUI.getAreaType(kToolbarId), CustomizableUI.TYPE_TOOLBAR, + "Area should be registered as toolbar"); + assertAreaPlacements(kToolbarId, [kButtonId]); + ok(!CustomizableUI.inDefaultState, "No longer in default state after toolbar is registered and visible."); + CustomizableUI.unregisterArea(kToolbarId, true); + toolbar.remove(); + ok(CustomizableUI.inDefaultState, "Everything should be in its default state."); + btn.remove(); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); + +function cleanup() { + let toolbar = document.getElementById(kToolbarId); + if (toolbar) { + toolbar.remove(); + } + let btn = document.getElementById(kButtonId) || + gNavToolbox.querySelector("#" + kButtonId); + if (btn) { + btn.remove(); + } +} diff --git a/browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js b/browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js new file mode 100644 index 000000000..944879a1b --- /dev/null +++ b/browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js @@ -0,0 +1,136 @@ +/* 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"; + +var button, menuButton; +/* Clicking a button should close the panel */ +add_task(function*() { + button = document.createElement("toolbarbutton"); + button.id = "browser_940307_button"; + button.setAttribute("label", "Button"); + PanelUI.contents.appendChild(button); + yield PanelUI.show(); + let hiddenAgain = promisePanelHidden(window); + EventUtils.synthesizeMouseAtCenter(button, {}); + yield hiddenAgain; + button.remove(); +}); + +/* Clicking a menu button should close the panel, opening the popup shouldn't. */ +add_task(function*() { + menuButton = document.createElement("toolbarbutton"); + menuButton.setAttribute("type", "menu-button"); + menuButton.id = "browser_940307_menubutton"; + menuButton.setAttribute("label", "Menu button"); + + let menuPopup = document.createElement("menupopup"); + menuPopup.id = "browser_940307_menupopup"; + + let menuItem = document.createElement("menuitem"); + menuItem.setAttribute("label", "Menu item"); + menuItem.id = "browser_940307_menuitem"; + + menuPopup.appendChild(menuItem); + menuButton.appendChild(menuPopup); + PanelUI.contents.appendChild(menuButton); + + yield PanelUI.show(); + let hiddenAgain = promisePanelHidden(window); + let innerButton = document.getAnonymousElementByAttribute(menuButton, "anonid", "button"); + EventUtils.synthesizeMouseAtCenter(innerButton, {}); + yield hiddenAgain; + + // Now click the dropmarker to show the menu + yield PanelUI.show(); + hiddenAgain = promisePanelHidden(window); + let menuShown = promisePanelElementShown(window, menuPopup); + let dropmarker = document.getAnonymousElementByAttribute(menuButton, "type", "menu-button"); + EventUtils.synthesizeMouseAtCenter(dropmarker, {}); + yield menuShown; + // Panel should stay open: + ok(isPanelUIOpen(), "Panel should still be open"); + let menuHidden = promisePanelElementHidden(window, menuPopup); + // Then click the menu item to close all the things + EventUtils.synthesizeMouseAtCenter(menuItem, {}); + yield menuHidden; + yield hiddenAgain; + menuButton.remove(); +}); + +add_task(function*() { + let searchbar = document.getElementById("searchbar"); + gCustomizeMode.addToPanel(searchbar); + let placement = CustomizableUI.getPlacementOfWidget("search-container"); + is(placement.area, CustomizableUI.AREA_PANEL, "Should be in panel"); + yield PanelUI.show(); + yield waitForCondition(() => "value" in searchbar && searchbar.value === ""); + + // Focusing a non-empty searchbox will cause us to open the + // autocomplete panel and search for suggestions, which would + // trigger network requests. Temporarily disable suggestions. + yield SpecialPowers.pushPrefEnv({set: [["browser.search.suggest.enabled", false]]}); + + searchbar.value = "foo"; + searchbar.focus(); + // Reaching into this context menu is pretty evil, but hey... it's a test. + let textbox = document.getAnonymousElementByAttribute(searchbar.textbox, "anonid", "textbox-input-box"); + let contextmenu = document.getAnonymousElementByAttribute(textbox, "anonid", "input-box-contextmenu"); + let contextMenuShown = promisePanelElementShown(window, contextmenu); + EventUtils.synthesizeMouseAtCenter(searchbar, {type: "contextmenu", button: 2}); + yield contextMenuShown; + + ok(isPanelUIOpen(), "Panel should still be open"); + + let selectAll = contextmenu.querySelector("[cmd='cmd_selectAll']"); + let contextMenuHidden = promisePanelElementHidden(window, contextmenu); + EventUtils.synthesizeMouseAtCenter(selectAll, {}); + yield contextMenuHidden; + + // Hide the suggestion panel. + searchbar.textbox.popup.hidePopup(); + + ok(isPanelUIOpen(), "Panel should still be open"); + + let hiddenPanelPromise = promisePanelHidden(window); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + yield hiddenPanelPromise; + ok(!isPanelUIOpen(), "Panel should no longer be open"); + + // We focused the search bar earlier - ensure we don't keep doing that. + gURLBar.select(); + + CustomizableUI.reset(); +}); + +add_task(function*() { + button = document.createElement("toolbarbutton"); + button.id = "browser_946166_button_disabled"; + button.setAttribute("disabled", "true"); + button.setAttribute("label", "Button"); + PanelUI.contents.appendChild(button); + yield PanelUI.show(); + EventUtils.synthesizeMouseAtCenter(button, {}); + is(PanelUI.panel.state, "open", "Popup stays open"); + button.removeAttribute("disabled"); + let hiddenAgain = promisePanelHidden(window); + EventUtils.synthesizeMouseAtCenter(button, {}); + yield hiddenAgain; + button.remove(); +}); + +registerCleanupFunction(function() { + if (button && button.parentNode) { + button.remove(); + } + if (menuButton && menuButton.parentNode) { + menuButton.remove(); + } + // Sadly this isn't task.jsm-enabled, so we can't wait for this to happen. But we should + // definitely close it here and hope it won't interfere with other tests. + // Of course, all the tests are meant to do this themselves, but if they fail... + if (isPanelUIOpen()) { + PanelUI.hide(); + } +}); diff --git a/browser/components/customizableui/test/browser_940946_removable_from_navbar_customizemode.js b/browser/components/customizableui/test/browser_940946_removable_from_navbar_customizemode.js new file mode 100644 index 000000000..c81b004c1 --- /dev/null +++ b/browser/components/customizableui/test/browser_940946_removable_from_navbar_customizemode.js @@ -0,0 +1,22 @@ +/* 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 kTestBtnId = "test-removable-navbar-customize-mode"; + +// Items without the removable attribute in the navbar should be considered non-removable +add_task(function*() { + let btn = createDummyXULButton(kTestBtnId, "Test removable in navbar in customize mode"); + document.getElementById("nav-bar").customizationTarget.appendChild(btn); + yield startCustomizing(); + ok(!CustomizableUI.isWidgetRemovable(kTestBtnId), "Widget should not be considered removable"); + yield endCustomizing(); + document.getElementById(kTestBtnId).remove(); +}); + +add_task(function* asyncCleanup() { + yield endCustomizing(); + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_941083_invalidate_wrapper_cache_createWidget.js b/browser/components/customizableui/test/browser_941083_invalidate_wrapper_cache_createWidget.js new file mode 100644 index 000000000..1d7f86fd2 --- /dev/null +++ b/browser/components/customizableui/test/browser_941083_invalidate_wrapper_cache_createWidget.js @@ -0,0 +1,31 @@ +/* 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"; + +// See https://bugzilla.mozilla.org/show_bug.cgi?id=941083 + +const kWidgetId = "test-invalidate-wrapper-cache"; + +// Check createWidget invalidates the widget cache +add_task(function() { + let groupWrapper = CustomizableUI.getWidget(kWidgetId); + ok(groupWrapper, "Should get group wrapper."); + let singleWrapper = groupWrapper.forWindow(window); + ok(singleWrapper, "Should get single wrapper."); + + CustomizableUI.createWidget({id: kWidgetId, label: "Test invalidating widgets caching"}); + + let newGroupWrapper = CustomizableUI.getWidget(kWidgetId); + ok(newGroupWrapper, "Should get a group wrapper again."); + isnot(newGroupWrapper, groupWrapper, "Wrappers shouldn't be the same."); + isnot(newGroupWrapper.provider, groupWrapper.provider, "Wrapper providers shouldn't be the same."); + + let newSingleWrapper = newGroupWrapper.forWindow(window); + isnot(newSingleWrapper, singleWrapper, "Single wrappers shouldn't be the same."); + isnot(newSingleWrapper.provider, singleWrapper.provider, "Single wrapper providers shouldn't be the same."); + + CustomizableUI.destroyWidget(kWidgetId); + ok(!CustomizableUI.getWidget(kWidgetId), "Shouldn't get a wrapper after destroying the widget."); +}); diff --git a/browser/components/customizableui/test/browser_942581_unregisterArea_keeps_placements.js b/browser/components/customizableui/test/browser_942581_unregisterArea_keeps_placements.js new file mode 100644 index 000000000..61adac982 --- /dev/null +++ b/browser/components/customizableui/test/browser_942581_unregisterArea_keeps_placements.js @@ -0,0 +1,106 @@ +/* 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 kToolbarName = "test-unregisterArea-placements-toolbar"; +const kTestWidgetPfx = "test-widget-for-unregisterArea-placements-"; +const kTestWidgetCount = 3; +registerCleanupFunction(removeCustomToolbars); + +// unregisterArea should keep placements by default and restore them when re-adding the area +add_task(function*() { + let widgetIds = []; + for (let i = 0; i < kTestWidgetCount; i++) { + let id = kTestWidgetPfx + i; + widgetIds.push(id); + let spec = {id: id, type: 'button', removable: true, label: "unregisterArea test", tooltiptext: "" + i}; + CustomizableUI.createWidget(spec); + } + for (let i = kTestWidgetCount; i < kTestWidgetCount * 2; i++) { + let id = kTestWidgetPfx + i; + widgetIds.push(id); + createDummyXULButton(id, "unregisterArea XUL test " + i); + } + let toolbarNode = createToolbarWithPlacements(kToolbarName, widgetIds); + checkAbstractAndRealPlacements(toolbarNode, widgetIds); + + // Now move one of them: + CustomizableUI.moveWidgetWithinArea(kTestWidgetPfx + kTestWidgetCount, 0); + // Clone the array so we know this is the modified one: + let modifiedWidgetIds = [...widgetIds]; + let movedWidget = modifiedWidgetIds.splice(kTestWidgetCount, 1)[0]; + modifiedWidgetIds.unshift(movedWidget); + + // Check it: + checkAbstractAndRealPlacements(toolbarNode, modifiedWidgetIds); + + // Then unregister + CustomizableUI.unregisterArea(kToolbarName); + + // Check we tell the outside world no dangerous things: + checkWidgetFates(widgetIds); + // Only then remove the real node + toolbarNode.remove(); + + // Now move one of the items to the palette, and another to the navbar: + let lastWidget = modifiedWidgetIds.pop(); + CustomizableUI.removeWidgetFromArea(lastWidget); + lastWidget = modifiedWidgetIds.pop(); + CustomizableUI.addWidgetToArea(lastWidget, CustomizableUI.AREA_NAVBAR); + + // Recreate ourselves with the default placements being the same: + toolbarNode = createToolbarWithPlacements(kToolbarName, widgetIds); + // Then check that after doing this, our actual placements match + // the modified list, not the default one. + checkAbstractAndRealPlacements(toolbarNode, modifiedWidgetIds); + + // Now remove completely: + CustomizableUI.unregisterArea(kToolbarName, true); + checkWidgetFates(modifiedWidgetIds); + toolbarNode.remove(); + + // One more time: + // Recreate ourselves with the default placements being the same: + toolbarNode = createToolbarWithPlacements(kToolbarName, widgetIds); + // Should now be back to default: + checkAbstractAndRealPlacements(toolbarNode, widgetIds); + CustomizableUI.unregisterArea(kToolbarName, true); + checkWidgetFates(widgetIds); + toolbarNode.remove(); + + // XXXgijs: ensure cleanup function doesn't barf: + gAddedToolbars.delete(kToolbarName); + + // Remove all the XUL widgets, destroy the others: + for (let widget of widgetIds) { + let widgetWrapper = CustomizableUI.getWidget(widget); + if (widgetWrapper.provider == CustomizableUI.PROVIDER_XUL) { + gNavToolbox.palette.querySelector("#" + widget).remove(); + } else { + CustomizableUI.destroyWidget(widget); + } + } +}); + +function checkAbstractAndRealPlacements(aNode, aExpectedPlacements) { + assertAreaPlacements(kToolbarName, aExpectedPlacements); + let physicalWidgetIds = Array.from(aNode.childNodes, (node) => node.id); + placementArraysEqual(aNode.id, physicalWidgetIds, aExpectedPlacements); +} + +function checkWidgetFates(aWidgetIds) { + for (let widget of aWidgetIds) { + ok(!CustomizableUI.getPlacementOfWidget(widget), "Widget should be in palette"); + ok(!document.getElementById(widget), "Widget should not be in the DOM"); + let widgetInPalette = !!gNavToolbox.palette.querySelector("#" + widget); + let widgetProvider = CustomizableUI.getWidget(widget).provider; + let widgetIsXULWidget = widgetProvider == CustomizableUI.PROVIDER_XUL; + is(widgetInPalette, widgetIsXULWidget, "Just XUL Widgets should be in the palette"); + } +} + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_943683_migration_test.js b/browser/components/customizableui/test/browser_943683_migration_test.js new file mode 100644 index 000000000..fe30df9e3 --- /dev/null +++ b/browser/components/customizableui/test/browser_943683_migration_test.js @@ -0,0 +1,50 @@ +/* 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 kWidgetId = "test-addonbar-migration"; +const kWidgetId2 = "test-addonbar-migration2"; + +var addonbar = document.getElementById(CustomizableUI.AREA_ADDONBAR); +var navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + +var btn; +var btn2; + +// Check we migrate normal stuff to the navbar +add_task(function*() { + btn = createDummyXULButton(kWidgetId, "Test"); + btn2 = createDummyXULButton(kWidgetId2, "Test2"); + addonbar.insertItem(btn.id); + ok(btn.parentNode == navbar.customizationTarget, "Button should end up in navbar"); + let migrationArray = addonbar.getMigratedItems(); + is(migrationArray.length, 1, "Should have migrated 1 item"); + is(migrationArray[0], kWidgetId, "Should have migrated our 1 item"); + + addonbar.currentSet = addonbar.currentSet + "," + kWidgetId2; + ok(btn2.parentNode == navbar.customizationTarget, "Second button should end up in the navbar"); + migrationArray = addonbar.getMigratedItems(); + is(migrationArray.length, 2, "Should have migrated 2 items"); + isnot(migrationArray.indexOf(kWidgetId2), -1, "Should have migrated our second item"); + + let otherWindow = yield openAndLoadWindow(undefined, true); + try { + let addonBar = otherWindow.document.getElementById("addon-bar"); + let otherMigrationArray = addonBar.getMigratedItems(); + is(migrationArray.length, otherMigrationArray.length, + "Other window should have the same number of migrated items."); + if (migrationArray.length == otherMigrationArray.length) { + for (let widget of migrationArray) { + isnot(otherMigrationArray.indexOf(widget), -1, + "Migrated widget " + widget + " should also be listed as migrated in the other window."); + } + } + } finally { + yield promiseWindowClosed(otherWindow); + } + btn.remove(); + btn2.remove(); + CustomizableUI.reset(); +}); diff --git a/browser/components/customizableui/test/browser_944887_destroyWidget_should_destroy_in_palette.js b/browser/components/customizableui/test/browser_944887_destroyWidget_should_destroy_in_palette.js new file mode 100644 index 000000000..a724b0c7f --- /dev/null +++ b/browser/components/customizableui/test/browser_944887_destroyWidget_should_destroy_in_palette.js @@ -0,0 +1,17 @@ +/* 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 kWidgetId = "test-destroy-in-palette"; + +// Check destroyWidget destroys the node if it's in the palette +add_task(function*() { + CustomizableUI.createWidget({id: kWidgetId, label: "Test destroying widgets in palette."}); + yield startCustomizing(); + yield endCustomizing(); + ok(gNavToolbox.palette.querySelector("#" + kWidgetId), "Widget still exists in palette."); + CustomizableUI.destroyWidget(kWidgetId); + ok(!gNavToolbox.palette.querySelector("#" + kWidgetId), "Widget no longer exists in palette."); +}); diff --git a/browser/components/customizableui/test/browser_945739_showInPrivateBrowsing_customize_mode.js b/browser/components/customizableui/test/browser_945739_showInPrivateBrowsing_customize_mode.js new file mode 100644 index 000000000..6b8acbee0 --- /dev/null +++ b/browser/components/customizableui/test/browser_945739_showInPrivateBrowsing_customize_mode.js @@ -0,0 +1,35 @@ +/* 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 kWidgetId = "test-private-browsing-customize-mode-widget"; + +// Add a widget via the API with showInPrivateBrowsing set to false +// and ensure it does not appear in the list of unused widgets in private +// windows. +add_task(function* testPrivateBrowsingCustomizeModeWidget() { + CustomizableUI.createWidget({ + id: kWidgetId, + showInPrivateBrowsing: false + }); + + let normalWidgetArray = CustomizableUI.getUnusedWidgets(gNavToolbox.palette); + normalWidgetArray = normalWidgetArray.map((w) => w.id); + ok(normalWidgetArray.indexOf(kWidgetId) > -1, + "Widget should appear as unused in non-private window"); + + let privateWindow = yield openAndLoadWindow({private: true}); + let privateWidgetArray = CustomizableUI.getUnusedWidgets(privateWindow.gNavToolbox.palette); + privateWidgetArray = privateWidgetArray.map((w) => w.id); + is(privateWidgetArray.indexOf(kWidgetId), -1, + "Widget should not appear as unused in private window"); + yield promiseWindowClosed(privateWindow); + + CustomizableUI.destroyWidget(kWidgetId); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_947914_button_addons.js b/browser/components/customizableui/test/browser_947914_button_addons.js new file mode 100644 index 000000000..b942ee771 --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_addons.js @@ -0,0 +1,33 @@ +/* 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"; + +var initialLocation = gBrowser.currentURI.spec; +var newTab = null; + +add_task(function*() { + info("Check addons button existence and functionality"); + + yield PanelUI.show(); + info("Menu panel was opened"); + + let addonsButton = document.getElementById("add-ons-button"); + ok(addonsButton, "Add-ons button exists in Panel Menu"); + addonsButton.click(); + + newTab = gBrowser.selectedTab; + yield waitForCondition(() => gBrowser.currentURI && + gBrowser.currentURI.spec == "about:addons"); + + let addonsPage = gBrowser.selectedBrowser.contentWindow.document. + getElementById("addons-page"); + ok(addonsPage, "Add-ons page was opened"); +}); + +add_task(function* asyncCleanup() { + gBrowser.addTab(initialLocation); + gBrowser.removeTab(gBrowser.selectedTab); + info("Tabs were restored"); +}); diff --git a/browser/components/customizableui/test/browser_947914_button_copy.js b/browser/components/customizableui/test/browser_947914_button_copy.js new file mode 100644 index 000000000..c778c956f --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_copy.js @@ -0,0 +1,59 @@ +/* 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"; + +var initialLocation = gBrowser.currentURI.spec; +var globalClipboard; + +add_task(function*() { + yield BrowserTestUtils.withNewTab({gBrowser, url: "about:blank"}, function*() { + info("Check copy button existence and functionality"); + + let testText = "copy text test"; + + gURLBar.focus(); + info("The URL bar was focused"); + yield PanelUI.show(); + info("Menu panel was opened"); + + let copyButton = document.getElementById("copy-button"); + ok(copyButton, "Copy button exists in Panel Menu"); + ok(copyButton.getAttribute("disabled"), "Copy button is initially disabled"); + + // copy text from URL bar + gURLBar.value = testText; + gURLBar.focus(); + gURLBar.select(); + yield PanelUI.show(); + info("Menu panel was opened"); + + ok(!copyButton.hasAttribute("disabled"), "Copy button is enabled when selecting"); + + copyButton.click(); + is(gURLBar.value, testText, "Selected text is unaltered when clicking copy"); + + // check that the text was added to the clipboard + let clipboard = Services.clipboard; + let transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); + globalClipboard = clipboard.kGlobalClipboard; + + transferable.init(null); + transferable.addDataFlavor("text/unicode"); + clipboard.getData(transferable, globalClipboard); + let str = {}, strLength = {}; + transferable.getTransferData("text/unicode", str, strLength); + let clipboardValue = ""; + + if (str.value) { + str.value.QueryInterface(Ci.nsISupportsString); + clipboardValue = str.value.data; + } + is(clipboardValue, testText, "Data was copied to the clipboard."); + }); +}); + +registerCleanupFunction(function cleanup() { + Services.clipboard.emptyClipboard(globalClipboard); +}); diff --git a/browser/components/customizableui/test/browser_947914_button_cut.js b/browser/components/customizableui/test/browser_947914_button_cut.js new file mode 100644 index 000000000..e6e614368 --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_cut.js @@ -0,0 +1,57 @@ +/* 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"; + +var initialLocation = gBrowser.currentURI.spec; +var globalClipboard; + +add_task(function*() { + yield BrowserTestUtils.withNewTab({gBrowser, url: "about:blank"}, function*() { + info("Check cut button existence and functionality"); + + let testText = "cut text test"; + + gURLBar.focus(); + yield PanelUI.show(); + info("Menu panel was opened"); + + let cutButton = document.getElementById("cut-button"); + ok(cutButton, "Cut button exists in Panel Menu"); + ok(cutButton.hasAttribute("disabled"), "Cut button is disabled"); + + // cut text from URL bar + gURLBar.value = testText; + gURLBar.focus(); + gURLBar.select(); + yield PanelUI.show(); + info("Menu panel was opened"); + + ok(!cutButton.hasAttribute("disabled"), "Cut button is enabled when selecting"); + cutButton.click(); + is(gURLBar.value, "", "Selected text is removed from source when clicking on cut"); + + // check that the text was added to the clipboard + let clipboard = Services.clipboard; + let transferable = Cc["@mozilla.org/widget/transferable;1"].createInstance(Ci.nsITransferable); + globalClipboard = clipboard.kGlobalClipboard; + + transferable.init(null); + transferable.addDataFlavor("text/unicode"); + clipboard.getData(transferable, globalClipboard); + let str = {}, strLength = {}; + transferable.getTransferData("text/unicode", str, strLength); + let clipboardValue = ""; + + if (str.value) { + str.value.QueryInterface(Ci.nsISupportsString); + clipboardValue = str.value.data; + } + is(clipboardValue, testText, "Data was copied to the clipboard."); + }); +}); + +registerCleanupFunction(function cleanup() { + Services.clipboard.emptyClipboard(globalClipboard); +}); diff --git a/browser/components/customizableui/test/browser_947914_button_find.js b/browser/components/customizableui/test/browser_947914_button_find.js new file mode 100644 index 000000000..cf3b79e34 --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_find.js @@ -0,0 +1,22 @@ +/* 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"; + +add_task(function*() { + info("Check find button existence and functionality"); + + yield PanelUI.show(); + info("Menu panel was opened"); + + let findButton = document.getElementById("find-button"); + ok(findButton, "Find button exists in Panel Menu"); + + findButton.click(); + ok(!gFindBar.hasAttribute("hidden"), "Findbar opened successfully"); + + // close find bar + gFindBar.close(); + info("Findbar was closed"); +}); diff --git a/browser/components/customizableui/test/browser_947914_button_history.js b/browser/components/customizableui/test/browser_947914_button_history.js new file mode 100644 index 000000000..64080fcc3 --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_history.js @@ -0,0 +1,24 @@ +/* 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"; + +add_task(function*() { + info("Check history button existence and functionality"); + + yield PanelUI.show(); + info("Menu panel was opened"); + + let historyButton = document.getElementById("history-panelmenu"); + ok(historyButton, "History button appears in Panel Menu"); + + historyButton.click(); + let historyPanel = document.getElementById("PanelUI-history"); + ok(historyPanel.getAttribute("current"), "History Panel is in view"); + + let panelHiddenPromise = promisePanelHidden(window); + PanelUI.hide(); + yield panelHiddenPromise + info("Menu panel was closed"); +}); diff --git a/browser/components/customizableui/test/browser_947914_button_newPrivateWindow.js b/browser/components/customizableui/test/browser_947914_button_newPrivateWindow.js new file mode 100644 index 000000000..c2006bef0 --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_newPrivateWindow.js @@ -0,0 +1,48 @@ +/* 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"; + +add_task(function*() { + info("Check private browsing button existence and functionality"); + + yield PanelUI.show(); + info("Menu panel was opened"); + + let windowWasHandled = false; + let privateWindow = null; + + let observerWindowOpened = { + observe: function(aSubject, aTopic, aData) { + if (aTopic == "domwindowopened") { + privateWindow = aSubject.QueryInterface(Components.interfaces.nsIDOMWindow); + privateWindow.addEventListener("load", function newWindowHandler() { + privateWindow.removeEventListener("load", newWindowHandler, false); + is(privateWindow.location.href, "chrome://browser/content/browser.xul", + "A new browser window was opened"); + ok(PrivateBrowsingUtils.isWindowPrivate(privateWindow), "Window is private"); + windowWasHandled = true; + }, false); + } + } + } + + Services.ww.registerNotification(observerWindowOpened); + + let privateBrowsingButton = document.getElementById("privatebrowsing-button"); + ok(privateBrowsingButton, "Private browsing button exists in Panel Menu"); + privateBrowsingButton.click(); + + try { + yield waitForCondition(() => windowWasHandled); + yield promiseWindowClosed(privateWindow); + info("The new private window was closed"); + } + catch (e) { + ok(false, "The new private browser window was not properly handled"); + } + finally { + Services.ww.unregisterNotification(observerWindowOpened); + } +}); diff --git a/browser/components/customizableui/test/browser_947914_button_newWindow.js b/browser/components/customizableui/test/browser_947914_button_newWindow.js new file mode 100644 index 000000000..47162ee86 --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_newWindow.js @@ -0,0 +1,47 @@ +/* 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"; + +add_task(function*() { + info("Check new window button existence and functionality"); + yield PanelUI.show(); + info("Menu panel was opened"); + + let windowWasHandled = false; + let newWindow = null; + + let observerWindowOpened = { + observe: function(aSubject, aTopic, aData) { + if (aTopic == "domwindowopened") { + newWindow = aSubject.QueryInterface(Components.interfaces.nsIDOMWindow); + newWindow.addEventListener("load", function newWindowHandler() { + newWindow.removeEventListener("load", newWindowHandler, false); + is(newWindow.location.href, "chrome://browser/content/browser.xul", + "A new browser window was opened"); + ok(!PrivateBrowsingUtils.isWindowPrivate(newWindow), "Window is not private"); + windowWasHandled = true; + }, false); + } + } + } + + Services.ww.registerNotification(observerWindowOpened); + + let newWindowButton = document.getElementById("new-window-button"); + ok(newWindowButton, "New Window button exists in Panel Menu"); + newWindowButton.click(); + + try { + yield waitForCondition(() => windowWasHandled); + yield promiseWindowClosed(newWindow); + info("The new window was closed"); + } + catch (e) { + ok(false, "The new browser window was not properly handled"); + } + finally { + Services.ww.unregisterNotification(observerWindowOpened); + } +}); diff --git a/browser/components/customizableui/test/browser_947914_button_paste.js b/browser/components/customizableui/test/browser_947914_button_paste.js new file mode 100644 index 000000000..fc83ead56 --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_paste.js @@ -0,0 +1,41 @@ +/* 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"; + +var initialLocation = gBrowser.currentURI.spec; +var globalClipboard; + +add_task(function*() { + yield BrowserTestUtils.withNewTab({gBrowser, url: "about:blank"}, function*() { + info("Check paste button existence and functionality"); + + let clipboard = Cc["@mozilla.org/widget/clipboardhelper;1"].getService(Ci.nsIClipboardHelper); + globalClipboard = Services.clipboard.kGlobalClipboard; + + yield PanelUI.show(); + info("Menu panel was opened"); + + let pasteButton = document.getElementById("paste-button"); + ok(pasteButton, "Paste button exists in Panel Menu"); + + // add text to clipboard + let text = "Sample text for testing"; + clipboard.copyString(text); + + // test paste button by pasting text to URL bar + gURLBar.focus(); + yield PanelUI.show(); + info("Menu panel was opened"); + + ok(!pasteButton.hasAttribute("disabled"), "Paste button is enabled"); + pasteButton.click(); + + is(gURLBar.value, text, "Text pasted successfully"); + }); +}); + +registerCleanupFunction(function cleanup() { + Services.clipboard.emptyClipboard(globalClipboard); +}); diff --git a/browser/components/customizableui/test/browser_947914_button_print.js b/browser/components/customizableui/test/browser_947914_button_print.js new file mode 100644 index 000000000..af7abcaeb --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_print.js @@ -0,0 +1,45 @@ +/* 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 isOSX = (Services.appinfo.OS === "Darwin"); + +add_task(function*() { + yield BrowserTestUtils.withNewTab({ + gBrowser, + url: "http://example.com/", + }, function* () { + info("Check print button existence and functionality"); + + yield PanelUI.show(); + info("Menu panel was opened"); + + yield waitForCondition(() => document.getElementById("print-button") != null); + + let printButton = document.getElementById("print-button"); + ok(printButton, "Print button exists in Panel Menu"); + + if (isOSX) { + let panelHiddenPromise = promisePanelHidden(window); + PanelUI.hide(); + yield panelHiddenPromise; + info("Menu panel was closed"); + } + else { + printButton.click(); + yield waitForCondition(() => gInPrintPreviewMode); + + ok(gInPrintPreviewMode, "Entered print preview mode"); + + // close print preview + if (gInPrintPreviewMode) { + PrintUtils.exitPrintPreview(); + yield waitForCondition(() => !window.gInPrintPreviewMode); + info("Exited print preview") + } + } + }); +}); + diff --git a/browser/components/customizableui/test/browser_947914_button_savePage.js b/browser/components/customizableui/test/browser_947914_button_savePage.js new file mode 100644 index 000000000..543ff3ca6 --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_savePage.js @@ -0,0 +1,20 @@ +/* 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"; + +add_task(function*() { + info("Check save page button existence"); + + yield PanelUI.show(); + info("Menu panel was opened"); + + let savePageButton = document.getElementById("save-page-button"); + ok(savePageButton, "Save Page button exists in Panel Menu"); + + let panelHiddenPromise = promisePanelHidden(window); + PanelUI.hide(); + yield panelHiddenPromise; + info("Menu panel was closed"); +}); diff --git a/browser/components/customizableui/test/browser_947914_button_zoomIn.js b/browser/components/customizableui/test/browser_947914_button_zoomIn.js new file mode 100644 index 000000000..4463d87d6 --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_zoomIn.js @@ -0,0 +1,37 @@ +/* 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"; + +var initialPageZoom = ZoomManager.zoom; + +add_task(function*() { + info("Check zoom in button existence and functionality"); + + is(initialPageZoom, 1, "Initial zoom factor should be 1"); + + yield PanelUI.show(); + info("Menu panel was opened"); + + let zoomInButton = document.getElementById("zoom-in-button"); + ok(zoomInButton, "Zoom in button exists in Panel Menu"); + + zoomInButton.click(); + let pageZoomLevel = parseInt(ZoomManager.zoom * 100); + let zoomResetButton = document.getElementById("zoom-reset-button"); + let expectedZoomLevel = parseInt(zoomResetButton.getAttribute("label"), 10); + ok(pageZoomLevel > 100 && pageZoomLevel == expectedZoomLevel, "Page zoomed in correctly"); + + // close the Panel + let panelHiddenPromise = promisePanelHidden(window); + PanelUI.hide(); + yield panelHiddenPromise; + info("Menu panel was closed"); +}); + +add_task(function* asyncCleanup() { + // reset zoom level + ZoomManager.zoom = initialPageZoom; + info("Zoom level was restored"); +}); diff --git a/browser/components/customizableui/test/browser_947914_button_zoomOut.js b/browser/components/customizableui/test/browser_947914_button_zoomOut.js new file mode 100644 index 000000000..f9f51ac9a --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_zoomOut.js @@ -0,0 +1,38 @@ +/* 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"; + +var initialPageZoom = ZoomManager.zoom; + +add_task(function*() { + info("Check zoom out button existence and functionality"); + + is(initialPageZoom, 1, "Initial zoom factor should be 1"); + + yield PanelUI.show(); + info("Menu panel was opened"); + + let zoomOutButton = document.getElementById("zoom-out-button"); + ok(zoomOutButton, "Zoom out button exists in Panel Menu"); + + zoomOutButton.click(); + let pageZoomLevel = Math.round(ZoomManager.zoom * 100); + + let zoomResetButton = document.getElementById("zoom-reset-button"); + let expectedZoomLevel = parseInt(zoomResetButton.getAttribute("label"), 10); + ok(pageZoomLevel < 100 && pageZoomLevel == expectedZoomLevel, "Page zoomed out correctly"); + + // close the panel + let panelHiddenPromise = promisePanelHidden(window); + PanelUI.hide(); + yield panelHiddenPromise; + info("Menu panel was closed"); +}); + +add_task(function* asyncCleanup() { + // reset zoom level + ZoomManager.zoom = initialPageZoom; + info("Zoom level was restored"); +}); diff --git a/browser/components/customizableui/test/browser_947914_button_zoomReset.js b/browser/components/customizableui/test/browser_947914_button_zoomReset.js new file mode 100644 index 000000000..372097665 --- /dev/null +++ b/browser/components/customizableui/test/browser_947914_button_zoomReset.js @@ -0,0 +1,40 @@ +/* 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"; + +var initialPageZoom = ZoomManager.zoom; + +add_task(function*() { + info("Check zoom reset button existence and functionality"); + + is(initialPageZoom, 1, "Page zoom reset correctly"); + ZoomManager.zoom = 0.5; + yield PanelUI.show(); + info("Menu panel was opened"); + + let zoomResetButton = document.getElementById("zoom-reset-button"); + ok(zoomResetButton, "Zoom reset button exists in Panel Menu"); + + zoomResetButton.click(); + yield new Promise(SimpleTest.executeSoon); + + let pageZoomLevel = Math.floor(ZoomManager.zoom * 100); + let expectedZoomLevel = 100; + let buttonZoomLevel = parseInt(zoomResetButton.getAttribute("label"), 10); + is(pageZoomLevel, expectedZoomLevel, "Page zoom reset correctly"); + is(pageZoomLevel, buttonZoomLevel, "Button displays the correct zoom level"); + + // close the panel + let panelHiddenPromise = promisePanelHidden(window); + PanelUI.hide(); + yield panelHiddenPromise; + info("Menu panel was closed"); +}); + +add_task(function* asyncCleanup() { + // reset zoom level + ZoomManager.zoom = initialPageZoom; + info("Zoom level was restored"); +}); diff --git a/browser/components/customizableui/test/browser_947987_removable_default.js b/browser/components/customizableui/test/browser_947987_removable_default.js new file mode 100644 index 000000000..98325ec2a --- /dev/null +++ b/browser/components/customizableui/test/browser_947987_removable_default.js @@ -0,0 +1,68 @@ +/* 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"; + +var kWidgetId = "test-removable-widget-default"; +const kNavBar = CustomizableUI.AREA_NAVBAR; +var widgetCounter = 0; + +registerCleanupFunction(removeCustomToolbars); + +// Sanity checks +add_task(function() { + let brokenSpec = {id: kWidgetId + (widgetCounter++), removable: false}; + SimpleTest.doesThrow(() => CustomizableUI.createWidget(brokenSpec), + "Creating non-removable widget without defaultArea should throw."); + + // Widget without removable set should be removable: + let wrapper = CustomizableUI.createWidget({id: kWidgetId + (widgetCounter++)}); + ok(CustomizableUI.isWidgetRemovable(wrapper.id), "Should be removable by default."); + CustomizableUI.destroyWidget(wrapper.id); +}); + +// Test non-removable widget with defaultArea +add_task(function*() { + // Non-removable widget with defaultArea should work: + let spec = {id: kWidgetId + (widgetCounter++), removable: false, + defaultArea: kNavBar}; + let widgetWrapper; + try { + widgetWrapper = CustomizableUI.createWidget(spec); + } catch (ex) { + ok(false, "Creating a non-removable widget with a default area should not throw."); + return; + } + + let placement = CustomizableUI.getPlacementOfWidget(spec.id); + ok(placement, "Widget should be placed."); + is(placement.area, kNavBar, "Widget should be in navbar"); + let singleWrapper = widgetWrapper.forWindow(window); + ok(singleWrapper, "Widget should exist in window."); + ok(singleWrapper.node, "Widget node should exist in window."); + let expectedParent = CustomizableUI.getCustomizeTargetForArea(kNavBar, window); + is(singleWrapper.node.parentNode, expectedParent, "Widget should be in navbar."); + + let otherWin = yield openAndLoadWindow(true); + placement = CustomizableUI.getPlacementOfWidget(spec.id); + ok(placement, "Widget should be placed."); + is(placement && placement.area, kNavBar, "Widget should be in navbar"); + + singleWrapper = widgetWrapper.forWindow(otherWin); + ok(singleWrapper, "Widget should exist in other window."); + if (singleWrapper) { + ok(singleWrapper.node, "Widget node should exist in other window."); + if (singleWrapper.node) { + let expectedParent = CustomizableUI.getCustomizeTargetForArea(kNavBar, otherWin); + is(singleWrapper.node.parentNode, expectedParent, + "Widget should be in navbar in other window."); + } + } + CustomizableUI.destroyWidget(spec.id); + yield promiseWindowClosed(otherWin); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_948985_non_removable_defaultArea.js b/browser/components/customizableui/test/browser_948985_non_removable_defaultArea.js new file mode 100644 index 000000000..456c9ed02 --- /dev/null +++ b/browser/components/customizableui/test/browser_948985_non_removable_defaultArea.js @@ -0,0 +1,32 @@ +/* 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/. */ + +const kWidgetId = "test-destroy-non-removable-defaultArea"; + +add_task(function() { + let spec = {id: kWidgetId, label: "Test non-removable defaultArea re-adding.", + removable: false, defaultArea: CustomizableUI.AREA_NAVBAR}; + CustomizableUI.createWidget(spec); + let placement = CustomizableUI.getPlacementOfWidget(kWidgetId); + ok(placement, "Should have placed the widget."); + is(placement && placement.area, CustomizableUI.AREA_NAVBAR, "Widget should be in navbar"); + CustomizableUI.destroyWidget(kWidgetId); + CustomizableUI.removeWidgetFromArea(kWidgetId); + + CustomizableUI.createWidget(spec); + ok(placement, "Should have placed the widget."); + is(placement && placement.area, CustomizableUI.AREA_NAVBAR, "Widget should be in navbar"); + CustomizableUI.destroyWidget(kWidgetId); + CustomizableUI.removeWidgetFromArea(kWidgetId); + + const kPrefCustomizationAutoAdd = "browser.uiCustomization.autoAdd"; + Services.prefs.setBoolPref(kPrefCustomizationAutoAdd, false); + CustomizableUI.createWidget(spec); + ok(placement, "Should have placed the widget."); + is(placement && placement.area, CustomizableUI.AREA_NAVBAR, "Widget should be in navbar"); + CustomizableUI.destroyWidget(kWidgetId); + CustomizableUI.removeWidgetFromArea(kWidgetId); + Services.prefs.clearUserPref(kPrefCustomizationAutoAdd); +}); + diff --git a/browser/components/customizableui/test/browser_952963_areaType_getter_no_area.js b/browser/components/customizableui/test/browser_952963_areaType_getter_no_area.js new file mode 100644 index 000000000..fc05a99fd --- /dev/null +++ b/browser/components/customizableui/test/browser_952963_areaType_getter_no_area.js @@ -0,0 +1,52 @@ +/* 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 kToolbarName = "test-unregisterArea-areaType"; +const kUnregisterAreaTestWidget = "test-widget-for-unregisterArea-areaType"; +const kTestWidget = "test-widget-no-area-areaType"; +registerCleanupFunction(removeCustomToolbars); + +function checkAreaType(widget) { + try { + is(widget.areaType, null, "areaType should be null"); + } catch (ex) { + info("Fetching areaType threw: " + ex); + ok(false, "areaType getter shouldn't throw."); + } +} + +// widget wrappers in unregisterArea'd areas and nowhere shouldn't throw when checking areaTypes. +add_task(function*() { + // Using the ID before it's been created will imply a XUL wrapper; we'll test + // an API-based wrapper below + let toolbarNode = createToolbarWithPlacements(kToolbarName, [kUnregisterAreaTestWidget]); + CustomizableUI.unregisterArea(kToolbarName); + toolbarNode.remove(); + + let w = CustomizableUI.getWidget(kUnregisterAreaTestWidget); + checkAreaType(w); + + w = CustomizableUI.getWidget(kTestWidget); + checkAreaType(w); + + let spec = {id: kUnregisterAreaTestWidget, type: 'button', removable: true, + label: "areaType test", tooltiptext: "areaType test"}; + CustomizableUI.createWidget(spec); + toolbarNode = createToolbarWithPlacements(kToolbarName, [kUnregisterAreaTestWidget]); + CustomizableUI.unregisterArea(kToolbarName); + toolbarNode.remove(); + w = CustomizableUI.getWidget(spec.id); + checkAreaType(w); + CustomizableUI.removeWidgetFromArea(kUnregisterAreaTestWidget); + checkAreaType(w); + // XXXgijs: ensure cleanup function doesn't barf: + gAddedToolbars.delete(kToolbarName); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); + diff --git a/browser/components/customizableui/test/browser_956602_remove_special_widget.js b/browser/components/customizableui/test/browser_956602_remove_special_widget.js new file mode 100644 index 000000000..f87b2e4c8 --- /dev/null +++ b/browser/components/customizableui/test/browser_956602_remove_special_widget.js @@ -0,0 +1,31 @@ +/* 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"; + + +// Adding a separator and then dragging it out of the navbar shouldn't throw +add_task(function*() { + try { + let navbar = document.getElementById("nav-bar"); + let separatorSelector = "toolbarseparator[id^=customizableui-special-separator]"; + ok(!navbar.querySelector(separatorSelector), "Shouldn't be a separator in the navbar"); + CustomizableUI.addWidgetToArea('separator', 'nav-bar'); + yield startCustomizing(); + let separator = navbar.querySelector(separatorSelector); + ok(separator, "There should be a separator in the navbar now."); + let palette = document.getElementById("customization-palette"); + simulateItemDrag(separator, palette); + ok(!palette.querySelector(separatorSelector), "No separator in the palette."); + } catch (ex) { + Cu.reportError(ex); + ok(false, "Shouldn't throw an exception moving an item to the navbar."); + } finally { + yield endCustomizing(); + } +}); + +add_task(function* asyncCleanup() { + resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_962069_drag_to_overflow_chevron.js b/browser/components/customizableui/test/browser_962069_drag_to_overflow_chevron.js new file mode 100644 index 000000000..7c4f6cfa4 --- /dev/null +++ b/browser/components/customizableui/test/browser_962069_drag_to_overflow_chevron.js @@ -0,0 +1,54 @@ +/* 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"; + +var originalWindowWidth; + +// Drag to overflow chevron should open the overflow panel. +add_task(function*() { + originalWindowWidth = window.outerWidth; + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + ok(!navbar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar."); + ok(CustomizableUI.inDefaultState, "Should start in default state."); + window.resizeTo(400, window.outerHeight); + yield waitForCondition(() => navbar.hasAttribute("overflowing")); + ok(navbar.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + + let widgetOverflowPanel = document.getElementById("widget-overflow"); + let panelShownPromise = promisePanelElementShown(window, widgetOverflowPanel); + let identityBox = document.getElementById("identity-box"); + let overflowChevron = document.getElementById("nav-bar-overflow-button"); + + // Listen for hiding immediately so we don't miss the event because of the + // async-ness of the 'shown' yield... + let panelHiddenPromise = promisePanelElementHidden(window, widgetOverflowPanel); + + var ds = Components.classes["@mozilla.org/widget/dragservice;1"]. + getService(Components.interfaces.nsIDragService); + + ds.startDragSession(); + try { + var [result, dataTransfer] = EventUtils.synthesizeDragOver(identityBox, overflowChevron); + + // Wait for showing panel before ending drag session. + yield panelShownPromise; + + EventUtils.synthesizeDropAfterDragOver(result, dataTransfer, overflowChevron); + } finally { + ds.endDragSession(true); + } + + info("Overflow panel is shown."); + + widgetOverflowPanel.hidePopup(); + yield panelHiddenPromise; +}); + +add_task(function*() { + window.resizeTo(originalWindowWidth, window.outerHeight); + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + yield waitForCondition(() => !navbar.hasAttribute("overflowing")); + ok(!navbar.hasAttribute("overflowing"), "Should not have an overflowing toolbar."); +}); diff --git a/browser/components/customizableui/test/browser_962884_opt_in_disable_hyphens.js b/browser/components/customizableui/test/browser_962884_opt_in_disable_hyphens.js new file mode 100644 index 000000000..cf2603999 --- /dev/null +++ b/browser/components/customizableui/test/browser_962884_opt_in_disable_hyphens.js @@ -0,0 +1,67 @@ +/* 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"; + +add_task(function*() { + const kNormalLabel = "Character Encoding"; + CustomizableUI.addWidgetToArea("characterencoding-button", CustomizableUI.AREA_NAVBAR); + let characterEncoding = document.getElementById("characterencoding-button"); + const kOriginalLabel = characterEncoding.getAttribute("label"); + characterEncoding.setAttribute("label", "\u00ad" + kNormalLabel); + CustomizableUI.addWidgetToArea("characterencoding-button", CustomizableUI.AREA_PANEL); + + yield PanelUI.show(); + + is(characterEncoding.getAttribute("auto-hyphens"), "off", + "Hyphens should be disabled if the ­ character is present in the label"); + let multilineText = document.getAnonymousElementByAttribute(characterEncoding, "class", "toolbarbutton-multiline-text"); + let multilineTextCS = getComputedStyle(multilineText); + is(multilineTextCS.MozHyphens, "manual", "-moz-hyphens should be set to manual when the ­ character is present.") + + let hiddenPanelPromise = promisePanelHidden(window); + PanelUI.toggle(); + yield hiddenPanelPromise; + + characterEncoding.setAttribute("label", kNormalLabel); + + yield PanelUI.show(); + + isnot(characterEncoding.getAttribute("auto-hyphens"), "off", + "Hyphens should not be disabled if the ­ character is not present in the label"); + multilineText = document.getAnonymousElementByAttribute(characterEncoding, "class", "toolbarbutton-multiline-text"); + multilineTextCS = getComputedStyle(multilineText); + is(multilineTextCS.MozHyphens, "auto", "-moz-hyphens should be set to auto by default.") + + hiddenPanelPromise = promisePanelHidden(window); + PanelUI.toggle(); + yield hiddenPanelPromise; + + characterEncoding.setAttribute("label", "\u00ad" + kNormalLabel); + CustomizableUI.removeWidgetFromArea("characterencoding-button"); + yield startCustomizing(); + + isnot(characterEncoding.getAttribute("auto-hyphens"), "off", + "Hyphens should not be disabled when the widget is in the palette"); + + gCustomizeMode.addToPanel(characterEncoding); + is(characterEncoding.getAttribute("auto-hyphens"), "off", + "Hyphens should be disabled if the ­ character is present in the label in customization mode"); + multilineText = document.getAnonymousElementByAttribute(characterEncoding, "class", "toolbarbutton-multiline-text"); + multilineTextCS = getComputedStyle(multilineText); + is(multilineTextCS.MozHyphens, "manual", "-moz-hyphens should be set to manual when the ­ character is present in customization mode.") + + yield endCustomizing(); + + CustomizableUI.addWidgetToArea("characterencoding-button", CustomizableUI.AREA_NAVBAR); + ok(!characterEncoding.hasAttribute("auto-hyphens"), + "Removing the widget from the panel should remove the auto-hyphens attribute"); + + characterEncoding.setAttribute("label", kOriginalLabel); +}); + +add_task(function* asyncCleanup() { + yield endCustomizing(); + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_963639_customizing_attribute_non_customizable_toolbar.js b/browser/components/customizableui/test/browser_963639_customizing_attribute_non_customizable_toolbar.js new file mode 100644 index 000000000..e5710c50a --- /dev/null +++ b/browser/components/customizableui/test/browser_963639_customizing_attribute_non_customizable_toolbar.js @@ -0,0 +1,34 @@ +/* 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 kToolbar = "test-toolbar-963639-non-customizable-customizing-attribute"; + +add_task(function*() { + info("Test for Bug 963639 - CustomizeMode _onToolbarVisibilityChange sets @customizing on non-customizable toolbars"); + + let toolbar = document.createElement("toolbar"); + toolbar.id = kToolbar; + gNavToolbox.appendChild(toolbar); + + let testToolbar = document.getElementById(kToolbar) + ok(testToolbar, "Toolbar was created."); + is(gNavToolbox.getElementsByAttribute("id", kToolbar).length, 1, + "Toolbar was added to the navigator toolbox"); + + toolbar.setAttribute("toolbarname", "NonCustomizableToolbarCustomizingAttribute"); + toolbar.setAttribute("collapsed", "true"); + + yield startCustomizing(); + window.setToolbarVisibility(toolbar, "true"); + isnot(toolbar.getAttribute("customizing"), "true", + "Toolbar doesn't have the customizing attribute"); + + yield endCustomizing(); + gNavToolbox.removeChild(toolbar); + + is(gNavToolbox.getElementsByAttribute("id", kToolbar).length, 0, + "Toolbar was removed from the navigator toolbox"); +}); diff --git a/browser/components/customizableui/test/browser_967000_button_charEncoding.js b/browser/components/customizableui/test/browser_967000_button_charEncoding.js new file mode 100644 index 000000000..0688ebbd6 --- /dev/null +++ b/browser/components/customizableui/test/browser_967000_button_charEncoding.js @@ -0,0 +1,62 @@ +/* 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 TEST_PAGE = "http://mochi.test:8888/browser/browser/components/customizableui/test/support/test_967000_charEncoding_page.html"; + +add_task(function*() { + info("Check Character Encoding button functionality"); + + // add the Character Encoding button to the panel + CustomizableUI.addWidgetToArea("characterencoding-button", + CustomizableUI.AREA_PANEL); + + // check the button's functionality + yield PanelUI.show(); + + let charEncodingButton = document.getElementById("characterencoding-button"); + ok(charEncodingButton, "The Character Encoding button was added to the Panel Menu"); + is(charEncodingButton.getAttribute("disabled"), "true", + "The Character encoding button is initially disabled"); + + let panelHidePromise = promisePanelHidden(window); + PanelUI.hide(); + yield panelHidePromise; + + let newTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE, true, true); + + yield PanelUI.show(); + ok(!charEncodingButton.hasAttribute("disabled"), "The Character encoding button gets enabled"); + let characterEncodingView = document.getElementById("PanelUI-characterEncodingView"); + let subviewShownPromise = subviewShown(characterEncodingView); + charEncodingButton.click(); + yield subviewShownPromise; + + ok(characterEncodingView.hasAttribute("current"), "The Character encoding panel is displayed"); + + let pinnedEncodings = document.getElementById("PanelUI-characterEncodingView-pinned"); + let charsetsList = document.getElementById("PanelUI-characterEncodingView-charsets"); + ok(pinnedEncodings, "Pinned charsets are available"); + ok(charsetsList, "Charsets list is available"); + + let checkedButtons = characterEncodingView.querySelectorAll("toolbarbutton[checked='true']"); + is(checkedButtons.length, 2, "There should be 2 checked items (1 charset, 1 detector)."); + is(checkedButtons[0].getAttribute("label"), "Unicode", "The unicode encoding is correctly selected"); + is(characterEncodingView.querySelectorAll("#PanelUI-characterEncodingView-autodetect toolbarbutton[checked='true']").length, + 1, + "There should be 1 checked detector."); + + panelHidePromise = promisePanelHidden(window); + PanelUI.hide(); + yield panelHidePromise; + + yield BrowserTestUtils.removeTab(newTab); +}); + +add_task(function* asyncCleanup() { + // reset the panel to the default state + yield resetCustomization(); + ok(CustomizableUI.inDefaultState, "The UI is in default state again."); +}); diff --git a/browser/components/customizableui/test/browser_967000_button_feeds.js b/browser/components/customizableui/test/browser_967000_button_feeds.js new file mode 100644 index 000000000..8f391941a --- /dev/null +++ b/browser/components/customizableui/test/browser_967000_button_feeds.js @@ -0,0 +1,60 @@ +/* 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 TEST_PAGE = "http://mochi.test:8888/browser/browser/components/customizableui/test/support/feeds_test_page.html"; +const TEST_FEED = "http://mochi.test:8888/browser/browser/components/customizableui/test/support/test-feed.xml" + +var newTab = null; +var initialLocation = gBrowser.currentURI.spec; + +add_task(function*() { + info("Check Subscribe button functionality"); + + // add the Subscribe button to the panel + CustomizableUI.addWidgetToArea("feed-button", + CustomizableUI.AREA_PANEL); + + // check the button's functionality + yield PanelUI.show(); + + let feedButton = document.getElementById("feed-button"); + ok(feedButton, "The Subscribe button was added to the Panel Menu"); + is(feedButton.getAttribute("disabled"), "true", "The Subscribe button is initially disabled"); + + let panelHidePromise = promisePanelHidden(window); + PanelUI.hide(); + yield panelHidePromise; + + newTab = gBrowser.selectedTab; + yield promiseTabLoadEvent(newTab, TEST_PAGE); + + yield PanelUI.show(); + + yield waitForCondition(() => !feedButton.hasAttribute("disabled")); + ok(!feedButton.hasAttribute("disabled"), "The Subscribe button gets enabled"); + + feedButton.click(); + yield promiseTabLoadEvent(newTab, TEST_FEED); + + is(gBrowser.currentURI.spec, TEST_FEED, "Subscribe page opened"); + ok(!isPanelUIOpen(), "Panel is closed"); + + if (isPanelUIOpen()) { + panelHidePromise = promisePanelHidden(window); + PanelUI.hide(); + yield panelHidePromise; + } +}); + +add_task(function* asyncCleanup() { + // reset the panel UI to the default state + yield resetCustomization(); + ok(CustomizableUI.inDefaultState, "The UI is in default state again."); + + // restore the initial location + gBrowser.addTab(initialLocation); + gBrowser.removeTab(newTab); +}); diff --git a/browser/components/customizableui/test/browser_967000_button_sync.js b/browser/components/customizableui/test/browser_967000_button_sync.js new file mode 100644 index 000000000..15a3235e0 --- /dev/null +++ b/browser/components/customizableui/test/browser_967000_button_sync.js @@ -0,0 +1,335 @@ +/* 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"; + +requestLongerTimeout(2); + +let {SyncedTabs} = Cu.import("resource://services-sync/SyncedTabs.jsm", {}); + +XPCOMUtils.defineLazyModuleGetter(this, "UITour", "resource:///modules/UITour.jsm"); + +// These are available on the widget implementation, but it seems impossible +// to grab that impl at runtime. +const DECKINDEX_TABS = 0; +const DECKINDEX_TABSDISABLED = 1; +const DECKINDEX_FETCHING = 2; +const DECKINDEX_NOCLIENTS = 3; + +var initialLocation = gBrowser.currentURI.spec; +var newTab = null; + +// A helper to notify there are new tabs. Returns a promise that is resolved +// once the UI has been updated. +function updateTabsPanel() { + let promiseTabsUpdated = promiseObserverNotified("synced-tabs-menu:test:tabs-updated"); + Services.obs.notifyObservers(null, SyncedTabs.TOPIC_TABS_CHANGED, null); + return promiseTabsUpdated; +} + +// This is the mock we use for SyncedTabs.jsm - tests may override various +// functions. +let mockedInternal = { + get isConfiguredToSyncTabs() { return true; }, + getTabClients() { return []; }, + syncTabs() {}, + hasSyncedThisSession: false, +}; + + +add_task(function* setup() { + let oldInternal = SyncedTabs._internal; + SyncedTabs._internal = mockedInternal; + + registerCleanupFunction(() => { + SyncedTabs._internal = oldInternal; + }); +}); + +// The test expects the about:preferences#sync page to open in the current tab +function* openPrefsFromMenuPanel(expectedPanelId, entryPoint) { + info("Check Sync button functionality"); + Services.prefs.setCharPref("identity.fxaccounts.remote.signup.uri", "http://example.com/"); + + // add the Sync button to the panel + CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL); + + // check the button's functionality + yield PanelUI.show(); + + if (entryPoint == "uitour") { + UITour.tourBrowsersByWindow.set(window, new Set()); + UITour.tourBrowsersByWindow.get(window).add(gBrowser.selectedBrowser); + } + + let syncButton = document.getElementById("sync-button"); + ok(syncButton, "The Sync button was added to the Panel Menu"); + + syncButton.click(); + let syncPanel = document.getElementById("PanelUI-remotetabs"); + ok(syncPanel.getAttribute("current"), "Sync Panel is in view"); + + // Sync is not configured - verify that state is reflected. + let subpanel = document.getElementById(expectedPanelId) + ok(!subpanel.hidden, "sync setup element is visible"); + + // Find and click the "setup" button. + let setupButton = subpanel.querySelector(".PanelUI-remotetabs-prefs-button"); + setupButton.click(); + + let deferred = Promise.defer(); + let handler = (e) => { + if (e.originalTarget != gBrowser.selectedBrowser.contentDocument || + e.target.location.href == "about:blank") { + info("Skipping spurious 'load' event for " + e.target.location.href); + return; + } + gBrowser.selectedBrowser.removeEventListener("load", handler, true); + deferred.resolve(); + } + gBrowser.selectedBrowser.addEventListener("load", handler, true); + + yield deferred.promise; + newTab = gBrowser.selectedTab; + + is(gBrowser.currentURI.spec, "about:preferences?entrypoint=" + entryPoint + "#sync", + "Firefox Sync preference page opened with `menupanel` entrypoint"); + ok(!isPanelUIOpen(), "The panel closed"); + + if (isPanelUIOpen()) { + let panelHidePromise = promisePanelHidden(window); + PanelUI.hide(); + yield panelHidePromise; + } +} + +function* asyncCleanup() { + Services.prefs.clearUserPref("identity.fxaccounts.remote.signup.uri"); + // reset the panel UI to the default state + yield resetCustomization(); + ok(CustomizableUI.inDefaultState, "The panel UI is in default state again."); + + // restore the tabs + gBrowser.addTab(initialLocation); + gBrowser.removeTab(newTab); + UITour.tourBrowsersByWindow.delete(window); +} + +// When Sync is not setup. +add_task(() => openPrefsFromMenuPanel("PanelUI-remotetabs-setupsync", "synced-tabs")); +add_task(asyncCleanup); + +// When Sync is configured in a "needs reauthentication" state. +add_task(function* () { + // configure our broadcasters so we are in the right state. + document.getElementById("sync-reauth-state").hidden = false; + document.getElementById("sync-setup-state").hidden = true; + document.getElementById("sync-syncnow-state").hidden = true; + yield openPrefsFromMenuPanel("PanelUI-remotetabs-reauthsync", "synced-tabs") +}); + +// Test the mobile promo links +add_task(function* () { + // change the preferences for the mobile links. + Services.prefs.setCharPref("identity.mobilepromo.android", "http://example.com/?os=android&tail="); + Services.prefs.setCharPref("identity.mobilepromo.ios", "http://example.com/?os=ios&tail="); + + mockedInternal.getTabClients = () => []; + mockedInternal.syncTabs = () => Promise.resolve(); + + document.getElementById("sync-reauth-state").hidden = true; + document.getElementById("sync-setup-state").hidden = true; + document.getElementById("sync-syncnow-state").hidden = false; + + CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL); + + let syncPanel = document.getElementById("PanelUI-remotetabs"); + let links = syncPanel.querySelectorAll(".remotetabs-promo-link"); + + is(links.length, 2, "found 2 links as expected"); + + // test each link and left and middle mouse buttons + for (let link of links) { + for (let button = 0; button < 2; button++) { + yield PanelUI.show(); + EventUtils.sendMouseEvent({ type: "click", button }, link, window); + // the panel should have been closed. + ok(!isPanelUIOpen(), "click closed the panel"); + // should be a new tab - wait for the load. + is(gBrowser.tabs.length, 2, "there's a new tab"); + yield new Promise(resolve => { + if (gBrowser.selectedBrowser.currentURI.spec == "about:blank") { + gBrowser.selectedBrowser.addEventListener("load", function listener(e) { + gBrowser.selectedBrowser.removeEventListener("load", listener, true); + resolve(); + }, true); + return; + } + // the new tab has already transitioned away from about:blank so we + // are good to go. + resolve(); + }); + + let os = link.getAttribute("mobile-promo-os"); + let expectedUrl = `http://example.com/?os=${os}&tail=synced-tabs`; + is(gBrowser.selectedBrowser.currentURI.spec, expectedUrl, "correct URL"); + gBrowser.removeTab(gBrowser.selectedTab); + } + } + + // test each link and right mouse button - should be a noop. + yield PanelUI.show(); + for (let link of links) { + EventUtils.sendMouseEvent({ type: "click", button: 2 }, link, window); + // the panel should still be open + ok(isPanelUIOpen(), "panel remains open after right-click"); + is(gBrowser.tabs.length, 1, "no new tab was opened"); + } + PanelUI.hide(); + + Services.prefs.clearUserPref("identity.mobilepromo.android"); + Services.prefs.clearUserPref("identity.mobilepromo.ios"); +}); + +// Test the "Sync Now" button +add_task(function* () { + mockedInternal.getTabClients = () => []; + mockedInternal.syncTabs = () => { + return Promise.resolve(); + } + + // configure our broadcasters so we are in the right state. + document.getElementById("sync-reauth-state").hidden = true; + document.getElementById("sync-setup-state").hidden = true; + document.getElementById("sync-syncnow-state").hidden = false; + + // add the Sync button to the panel + CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL); + yield PanelUI.show(); + document.getElementById("sync-button").click(); + let syncPanel = document.getElementById("PanelUI-remotetabs"); + ok(syncPanel.getAttribute("current"), "Sync Panel is in view"); + + let subpanel = document.getElementById("PanelUI-remotetabs-main") + ok(!subpanel.hidden, "main pane is visible"); + let deck = document.getElementById("PanelUI-remotetabs-deck"); + + // The widget is still fetching tabs, as we've neutered everything that + // provides them + is(deck.selectedIndex, DECKINDEX_FETCHING, "first deck entry is visible"); + + let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow"); + + let didSync = false; + let oldDoSync = gSyncUI.doSync; + gSyncUI.doSync = function() { + didSync = true; + mockedInternal.hasSyncedThisSession = true; + gSyncUI.doSync = oldDoSync; + } + syncNowButton.click(); + ok(didSync, "clicking the button called the correct function"); + + // Tell the widget there are tabs available, but with zero clients. + mockedInternal.getTabClients = () => { + return Promise.resolve([]); + } + yield updateTabsPanel(); + // The UI should be showing the "no clients" pane. + is(deck.selectedIndex, DECKINDEX_NOCLIENTS, "no-clients deck entry is visible"); + + // Tell the widget there are tabs available - we have 3 clients, one with no + // tabs. + mockedInternal.getTabClients = () => { + return Promise.resolve([ + { + id: "guid_mobile", + type: "client", + name: "My Phone", + tabs: [], + }, + { + id: "guid_desktop", + type: "client", + name: "My Desktop", + tabs: [ + { + title: "http://example.com/10", + lastUsed: 10, // the most recent + }, + { + title: "http://example.com/1", + lastUsed: 1, // the least recent. + }, + { + title: "http://example.com/5", + lastUsed: 5, + }, + ], + }, + { + id: "guid_second_desktop", + name: "My Other Desktop", + tabs: [ + { + title: "http://example.com/6", + lastUsed: 6, + } + ], + }, + ]); + }; + yield updateTabsPanel(); + + // The UI should be showing tabs! + is(deck.selectedIndex, DECKINDEX_TABS, "no-clients deck entry is visible"); + let tabList = document.getElementById("PanelUI-remotetabs-tabslist"); + let node = tabList.firstChild; + // First entry should be the client with the most-recent tab. + is(node.getAttribute("itemtype"), "client", "node is a client entry"); + is(node.textContent, "My Desktop", "correct client"); + // Next entry is the most-recent tab + node = node.nextSibling; + is(node.getAttribute("itemtype"), "tab", "node is a tab"); + is(node.getAttribute("label"), "http://example.com/10"); + + // Next entry is the next-most-recent tab + node = node.nextSibling; + is(node.getAttribute("itemtype"), "tab", "node is a tab"); + is(node.getAttribute("label"), "http://example.com/5"); + + // Next entry is the least-recent tab from the first client. + node = node.nextSibling; + is(node.getAttribute("itemtype"), "tab", "node is a tab"); + is(node.getAttribute("label"), "http://example.com/1"); + + // Next is a menuseparator between the clients. + node = node.nextSibling; + is(node.nodeName, "menuseparator"); + + // Next is the client with 1 tab. + node = node.nextSibling; + is(node.getAttribute("itemtype"), "client", "node is a client entry"); + is(node.textContent, "My Other Desktop", "correct client"); + // Its single tab + node = node.nextSibling; + is(node.getAttribute("itemtype"), "tab", "node is a tab"); + is(node.getAttribute("label"), "http://example.com/6"); + + // Next is a menuseparator between the clients. + node = node.nextSibling; + is(node.nodeName, "menuseparator"); + + // Next is the client with no tab. + node = node.nextSibling; + is(node.getAttribute("itemtype"), "client", "node is a client entry"); + is(node.textContent, "My Phone", "correct client"); + // There is a single node saying there's no tabs for the client. + node = node.nextSibling; + is(node.nodeName, "label", "node is a label"); + is(node.getAttribute("itemtype"), "", "node is neither a tab nor a client"); + + node = node.nextSibling; + is(node, null, "no more entries"); +}); diff --git a/browser/components/customizableui/test/browser_968447_bookmarks_toolbar_items_in_panel.js b/browser/components/customizableui/test/browser_968447_bookmarks_toolbar_items_in_panel.js new file mode 100644 index 000000000..88c30bf81 --- /dev/null +++ b/browser/components/customizableui/test/browser_968447_bookmarks_toolbar_items_in_panel.js @@ -0,0 +1,65 @@ +/* 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"; + +// Bug 968447 - The Bookmarks Toolbar Items doesn't appear as a +// normal menu panel button in new windows. +add_task(function*() { + const buttonId = "bookmarks-toolbar-placeholder"; + yield startCustomizing(); + CustomizableUI.addWidgetToArea("personal-bookmarks", CustomizableUI.AREA_PANEL); + yield endCustomizing(); + + yield PanelUI.show(); + + let bookmarksToolbarPlaceholder = document.getElementById(buttonId); + ok(bookmarksToolbarPlaceholder.classList.contains("toolbarbutton-1"), + "Button should have toolbarbutton-1 class"); + is(bookmarksToolbarPlaceholder.getAttribute("wrap"), "true", + "Button should have the 'wrap' attribute"); + + info("Waiting for panel to close"); + let panelHiddenPromise = promisePanelHidden(window); + PanelUI.hide(); + yield panelHiddenPromise; + + info("Waiting for window to open"); + let newWin = yield openAndLoadWindow({}, true); + + info("Waiting for panel in new window to open"); + let hideTrace = function() { + info(new Error().stack); + info("Panel was hidden."); + }; + newWin.PanelUI.panel.addEventListener("popuphidden", hideTrace); + + yield newWin.PanelUI.show(); + let newWinBookmarksToolbarPlaceholder = newWin.document.getElementById(buttonId); + ok(newWinBookmarksToolbarPlaceholder.classList.contains("toolbarbutton-1"), + "Button in new window should have toolbarbutton-1 class"); + is(newWinBookmarksToolbarPlaceholder.getAttribute("wrap"), "true", + "Button in new window should have 'wrap' attribute"); + + newWin.PanelUI.panel.removeEventListener("popuphidden", hideTrace); + // XXXgijs on Linux, we're sometimes seeing the panel being hidden early + // in the newly created window, probably because something else steals focus. + if (newWin.PanelUI.panel.state != "closed") { + info("Panel is still open in new window, waiting for it to close"); + panelHiddenPromise = promisePanelHidden(newWin); + newWin.PanelUI.hide(); + yield panelHiddenPromise; + } else { + info("panel was already closed"); + } + + info("Waiting for new window to close"); + yield promiseWindowClosed(newWin); +}); + +add_task(function* asyncCleanUp() { + yield endCustomizing(); + CustomizableUI.reset(); +}); + diff --git a/browser/components/customizableui/test/browser_968565_insert_before_hidden_items.js b/browser/components/customizableui/test/browser_968565_insert_before_hidden_items.js new file mode 100644 index 000000000..f7504fc41 --- /dev/null +++ b/browser/components/customizableui/test/browser_968565_insert_before_hidden_items.js @@ -0,0 +1,56 @@ +/* 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 kHidden1Id = "test-hidden-button-1"; +const kHidden2Id = "test-hidden-button-2"; + +var navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + +// When we drag an item onto a customizable area, and not over a specific target, we +// should assume that we're appending them to the area. If doing so, we should scan +// backwards over any hidden items and insert the item before those hidden items. +add_task(function*() { + ok(CustomizableUI.inDefaultState, "Should be in the default state"); + + // Iterate backwards over the items in the nav-bar until we find the first + // one that is not hidden. + let placements = CustomizableUI.getWidgetsInArea(CustomizableUI.AREA_NAVBAR); + let lastVisible = null; + for (let widgetGroup of placements.reverse()) { + let widget = widgetGroup.forWindow(window); + if (widget && widget.node && !widget.node.hidden) { + lastVisible = widget.node; + break; + } + } + + if (!lastVisible) { + ok(false, "Apparently, there are no visible items in the nav-bar."); + } + + info("The last visible item in the nav-bar has ID: " + lastVisible.id); + + let hidden1 = createDummyXULButton(kHidden1Id, "You can't see me"); + let hidden2 = createDummyXULButton(kHidden2Id, "You can't see me either."); + hidden1.hidden = hidden2.hidden = true; + + // Make sure we have some hidden items at the end of the nav-bar. + navbar.insertItem(hidden1.id); + navbar.insertItem(hidden2.id); + + // Drag an item and drop it onto the nav-bar customization target, but + // not over a particular item. + yield startCustomizing(); + let downloadsButton = document.getElementById("downloads-button"); + simulateItemDrag(downloadsButton, navbar.customizationTarget); + + yield endCustomizing(); + + is(downloadsButton.previousSibling.id, lastVisible.id, + "The downloads button should be placed after the last visible item."); + + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_969427_recreate_destroyed_widget_after_reset.js b/browser/components/customizableui/test/browser_969427_recreate_destroyed_widget_after_reset.js new file mode 100644 index 000000000..b5479fcb7 --- /dev/null +++ b/browser/components/customizableui/test/browser_969427_recreate_destroyed_widget_after_reset.js @@ -0,0 +1,34 @@ +/* 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"; + +function getPlacementArea(id) { + let placement = CustomizableUI.getPlacementOfWidget(id); + return placement && placement.area; +} + +// Check that a destroyed widget recreated after a reset call goes to +// the navigation bar. +add_task(function() { + const kWidgetId = "test-recreate-after-reset"; + let spec = {id: kWidgetId, label: "Test re-create after reset.", + removable: true, defaultArea: CustomizableUI.AREA_NAVBAR}; + + CustomizableUI.createWidget(spec); + is(getPlacementArea(kWidgetId), CustomizableUI.AREA_NAVBAR, + "widget is in the navigation bar"); + + CustomizableUI.destroyWidget(kWidgetId); + isnot(getPlacementArea(kWidgetId), CustomizableUI.AREA_NAVBAR, + "widget removed from the navigation bar"); + + CustomizableUI.reset(); + + CustomizableUI.createWidget(spec); + is(getPlacementArea(kWidgetId), CustomizableUI.AREA_NAVBAR, + "widget recreated and added back to the nav bar"); + + CustomizableUI.destroyWidget(kWidgetId); +}); diff --git a/browser/components/customizableui/test/browser_969661_character_encoding_navbar_disabled.js b/browser/components/customizableui/test/browser_969661_character_encoding_navbar_disabled.js new file mode 100644 index 000000000..6f057a100 --- /dev/null +++ b/browser/components/customizableui/test/browser_969661_character_encoding_navbar_disabled.js @@ -0,0 +1,26 @@ +/* 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"; + + +// Adding the character encoding menu to the panel, exiting customize mode, +// and moving it to the nav-bar should have it enabled, not disabled. +add_task(function*() { + yield startCustomizing(); + CustomizableUI.addWidgetToArea("characterencoding-button", "PanelUI-contents"); + yield endCustomizing(); + yield PanelUI.show(); + let panelHiddenPromise = promisePanelHidden(window); + PanelUI.hide(); + yield panelHiddenPromise; + CustomizableUI.addWidgetToArea("characterencoding-button", 'nav-bar'); + let button = document.getElementById("characterencoding-button"); + ok(!button.hasAttribute("disabled"), "Button shouldn't be disabled"); +}); + +add_task(function asyncCleanup() { + resetCustomization(); +}); + diff --git a/browser/components/customizableui/test/browser_970511_undo_restore_default.js b/browser/components/customizableui/test/browser_970511_undo_restore_default.js new file mode 100644 index 000000000..e7b3ca674 --- /dev/null +++ b/browser/components/customizableui/test/browser_970511_undo_restore_default.js @@ -0,0 +1,128 @@ +/* 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"; + +requestLongerTimeout(2); + +// Restoring default should reset theme and show an "undo" option which undoes the restoring operation. +add_task(function*() { + let homeButtonId = "home-button"; + CustomizableUI.removeWidgetFromArea(homeButtonId); + yield startCustomizing(); + ok(!CustomizableUI.inDefaultState, "Not in default state to begin with"); + is(CustomizableUI.getPlacementOfWidget(homeButtonId), null, "Home button is in palette"); + let undoResetButton = document.getElementById("customization-undo-reset-button"); + is(undoResetButton.hidden, true, "The undo button is hidden before reset"); + + let themesButton = document.getElementById("customization-lwtheme-button"); + let popup = document.getElementById("customization-lwtheme-menu"); + let popupShownPromise = popupShown(popup); + EventUtils.synthesizeMouseAtCenter(themesButton, {}); + info("Clicked on themes button"); + yield popupShownPromise; + + let recommendedHeader = document.getElementById("customization-lwtheme-menu-recommended"); + let firstLWTheme = recommendedHeader.nextSibling; + let firstLWThemeId = firstLWTheme.theme.id; + let themeChangedPromise = promiseObserverNotified("lightweight-theme-changed"); + firstLWTheme.doCommand(); + info("Clicked on first theme"); + yield themeChangedPromise; + + is(LightweightThemeManager.currentTheme.id, firstLWThemeId, "Theme changed to first option"); + + yield gCustomizeMode.reset(); + + ok(CustomizableUI.inDefaultState, "In default state after reset"); + is(undoResetButton.hidden, false, "The undo button is visible after reset"); + is(LightweightThemeManager.currentTheme, null, "Theme reset to default"); + + yield gCustomizeMode.undoReset() + + is(LightweightThemeManager.currentTheme.id, firstLWThemeId, "Theme has been reset from default to original choice"); + ok(!CustomizableUI.inDefaultState, "Not in default state after undo-reset"); + is(undoResetButton.hidden, true, "The undo button is hidden after clicking on the undo button"); + is(CustomizableUI.getPlacementOfWidget(homeButtonId), null, "Home button is in palette"); + + yield gCustomizeMode.reset(); +}); + +// Performing an action after a reset will hide the reset button. +add_task(function*() { + let homeButtonId = "home-button"; + CustomizableUI.removeWidgetFromArea(homeButtonId); + ok(!CustomizableUI.inDefaultState, "Not in default state to begin with"); + is(CustomizableUI.getPlacementOfWidget(homeButtonId), null, "Home button is in palette"); + let undoResetButton = document.getElementById("customization-undo-reset-button"); + is(undoResetButton.hidden, true, "The undo button is hidden before reset"); + + yield gCustomizeMode.reset(); + + ok(CustomizableUI.inDefaultState, "In default state after reset"); + is(undoResetButton.hidden, false, "The undo button is visible after reset"); + + CustomizableUI.addWidgetToArea(homeButtonId, CustomizableUI.AREA_PANEL); + is(undoResetButton.hidden, true, "The undo button is hidden after another change"); +}); + +// "Restore defaults", exiting customize, and re-entering shouldn't show the Undo button +add_task(function*() { + let undoResetButton = document.getElementById("customization-undo-reset-button"); + is(undoResetButton.hidden, true, "The undo button is hidden before a reset"); + ok(!CustomizableUI.inDefaultState, "The browser should not be in default state"); + yield gCustomizeMode.reset(); + + is(undoResetButton.hidden, false, "The undo button is visible after a reset"); + yield endCustomizing(); + yield startCustomizing(); + is(undoResetButton.hidden, true, "The undo reset button should be hidden after entering customization mode"); +}); + +// Bug 971626 - Restore Defaults should collapse the Title Bar +add_task(function*() { + if (Services.appinfo.OS != "WINNT" && + Services.appinfo.OS != "Darwin") { + return; + } + let prefName = "browser.tabs.drawInTitlebar"; + let defaultValue = Services.prefs.getBoolPref(prefName); + let restoreDefaultsButton = document.getElementById("customization-reset-button"); + let titleBarButton = document.getElementById("customization-titlebar-visibility-button"); + let undoResetButton = document.getElementById("customization-undo-reset-button"); + ok(CustomizableUI.inDefaultState, "Should be in default state at start of test"); + ok(restoreDefaultsButton.disabled, "Restore defaults button should be disabled when in default state"); + is(titleBarButton.hasAttribute("checked"), !defaultValue, "Title bar button should reflect pref value"); + is(undoResetButton.hidden, true, "Undo reset button should be hidden at start of test"); + + Services.prefs.setBoolPref(prefName, !defaultValue); + ok(!restoreDefaultsButton.disabled, "Restore defaults button should be enabled when pref changed"); + is(titleBarButton.hasAttribute("checked"), defaultValue, "Title bar button should reflect changed pref value"); + ok(!CustomizableUI.inDefaultState, "With titlebar flipped, no longer default"); + is(undoResetButton.hidden, true, "Undo reset button should be hidden after pref change"); + + yield gCustomizeMode.reset(); + ok(restoreDefaultsButton.disabled, "Restore defaults button should be disabled after reset"); + is(titleBarButton.hasAttribute("checked"), !defaultValue, "Title bar button should reflect default value after reset"); + is(Services.prefs.getBoolPref(prefName), defaultValue, "Reset should reset drawInTitlebar"); + ok(CustomizableUI.inDefaultState, "In default state after titlebar reset"); + is(undoResetButton.hidden, false, "Undo reset button should be visible after reset"); + ok(!undoResetButton.disabled, "Undo reset button should be enabled after reset"); + + yield gCustomizeMode.undoReset(); + ok(!restoreDefaultsButton.disabled, "Restore defaults button should be enabled after undo-reset"); + is(titleBarButton.hasAttribute("checked"), defaultValue, "Title bar button should reflect undo-reset value"); + ok(!CustomizableUI.inDefaultState, "No longer in default state after undo"); + is(Services.prefs.getBoolPref(prefName), !defaultValue, "Undo-reset goes back to previous pref value"); + is(undoResetButton.hidden, true, "Undo reset button should be hidden after undo-reset clicked"); + + Services.prefs.clearUserPref(prefName); + ok(CustomizableUI.inDefaultState, "In default state after pref cleared"); + is(undoResetButton.hidden, true, "Undo reset button should be hidden at end of test"); +}); + +add_task(function* asyncCleanup() { + yield gCustomizeMode.reset(); + yield endCustomizing(); +}); diff --git a/browser/components/customizableui/test/browser_972267_customizationchange_events.js b/browser/components/customizableui/test/browser_972267_customizationchange_events.js new file mode 100644 index 000000000..b37dbe954 --- /dev/null +++ b/browser/components/customizableui/test/browser_972267_customizationchange_events.js @@ -0,0 +1,46 @@ +/* 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"; + +// Create a new window, then move the home button to the menu and check both windows have +// customizationchange events fire on the toolbox: +add_task(function*() { + let newWindow = yield openAndLoadWindow(); + let otherToolbox = newWindow.gNavToolbox; + + let handlerCalledCount = 0; + let handler = (ev) => { + handlerCalledCount++; + }; + + let homeButton = document.getElementById("home-button"); + + gNavToolbox.addEventListener("customizationchange", handler); + otherToolbox.addEventListener("customizationchange", handler); + + gCustomizeMode.addToPanel(homeButton); + + is(handlerCalledCount, 2, "Should be called for both windows."); + + // If the test is run in isolation and the panel has never been open, + // the button will be in the palette. Deal with this case: + if (homeButton.parentNode.id == "BrowserToolbarPalette") { + yield PanelUI.ensureReady(); + isnot(homeButton.parentNode.id, "BrowserToolbarPalette", "Home button should now be in panel"); + } + + handlerCalledCount = 0; + gCustomizeMode.addToToolbar(homeButton); + is(handlerCalledCount, 2, "Should be called for both windows."); + + gNavToolbox.removeEventListener("customizationchange", handler); + otherToolbox.removeEventListener("customizationchange", handler); + + yield promiseWindowClosed(newWindow); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_973641_button_addon.js b/browser/components/customizableui/test/browser_973641_button_addon.js new file mode 100755 index 000000000..796bf3d0e --- /dev/null +++ b/browser/components/customizableui/test/browser_973641_button_addon.js @@ -0,0 +1,71 @@ +/* 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 kButton = "test_button_for_addon"; +var initialLocation = gBrowser.currentURI.spec; + +add_task(function*() { + info("Check addon button functionality"); + + // create mocked addon button on the navigation bar + let widgetSpec = { + id: kButton, + type: 'button', + onClick: function() { + gBrowser.selectedTab = gBrowser.addTab("about:addons"); + } + }; + CustomizableUI.createWidget(widgetSpec); + CustomizableUI.addWidgetToArea(kButton, CustomizableUI.AREA_NAVBAR); + + // check the button's functionality in navigation bar + let addonButton = document.getElementById(kButton); + let navBar = document.getElementById("nav-bar"); + ok(addonButton, "Addon button exists"); + ok(navBar.contains(addonButton), "Addon button is in the navbar"); + yield checkButtonFunctionality(addonButton); + + resetTabs(); + + // move the add-on button in the Panel Menu + CustomizableUI.addWidgetToArea(kButton, CustomizableUI.AREA_PANEL); + ok(!navBar.contains(addonButton), "Addon button was removed from the browser bar"); + + // check the addon button's functionality in the Panel Menu + yield PanelUI.show(); + var panelMenu = document.getElementById("PanelUI-mainView"); + let addonButtonInPanel = panelMenu.getElementsByAttribute("id", kButton); + ok(panelMenu.contains(addonButton), "Addon button was added to the Panel Menu"); + yield checkButtonFunctionality(addonButtonInPanel[0]); +}); + +add_task(function* asyncCleanup() { + resetTabs(); + + // reset the UI to the default state + yield resetCustomization(); + ok(CustomizableUI.inDefaultState, "The UI is in default state again."); + + // destroy the widget + CustomizableUI.destroyWidget(kButton); +}); + +function resetTabs() { + // close all opened tabs + while (gBrowser.tabs.length > 1) { + gBrowser.removeTab(gBrowser.selectedTab); + } + + // restore the initial tab + gBrowser.addTab(initialLocation); + gBrowser.removeTab(gBrowser.selectedTab); +} + +function* checkButtonFunctionality(aButton) { + aButton.click(); + yield waitForCondition(() => gBrowser.currentURI && + gBrowser.currentURI.spec == "about:addons"); +} diff --git a/browser/components/customizableui/test/browser_973932_addonbar_currentset.js b/browser/components/customizableui/test/browser_973932_addonbar_currentset.js new file mode 100644 index 000000000..66fa6ef47 --- /dev/null +++ b/browser/components/customizableui/test/browser_973932_addonbar_currentset.js @@ -0,0 +1,30 @@ +/* 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"; + +var addonbarID = CustomizableUI.AREA_ADDONBAR; +var addonbar = document.getElementById(addonbarID); + +// Check that currentset is correctly updated after a reset: +add_task(function*() { + let placements = CustomizableUI.getWidgetIdsInArea(addonbarID); + is(placements.join(','), addonbar.getAttribute("currentset"), "Addon-bar currentset should match default placements"); + ok(CustomizableUI.inDefaultState, "Should be in default state"); + info("Adding a spring to add-on bar shim"); + CustomizableUI.addWidgetToArea("spring", addonbarID, 1); + ok(addonbar.getElementsByTagName("toolbarspring").length, "There should be a spring in the toolbar"); + ok(!CustomizableUI.inDefaultState, "Should no longer be in default state"); + placements = CustomizableUI.getWidgetIdsInArea(addonbarID); + is(placements.join(','), addonbar.getAttribute("currentset"), "Addon-bar currentset should match placements after spring addition"); + + yield startCustomizing(); + yield gCustomizeMode.reset(); + ok(CustomizableUI.inDefaultState, "Should be in default state after reset"); + placements = CustomizableUI.getWidgetIdsInArea(addonbarID); + is(placements.join(','), addonbar.getAttribute("currentset"), "Addon-bar currentset should match default placements after reset"); + ok(!addonbar.getElementsByTagName("toolbarspring").length, "There should be no spring in the toolbar"); + yield endCustomizing(); +}); + diff --git a/browser/components/customizableui/test/browser_975719_customtoolbars_behaviour.js b/browser/components/customizableui/test/browser_975719_customtoolbars_behaviour.js new file mode 100644 index 000000000..73fc7c1ff --- /dev/null +++ b/browser/components/customizableui/test/browser_975719_customtoolbars_behaviour.js @@ -0,0 +1,145 @@ +/* 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"; + +requestLongerTimeout(2); + +const kXULWidgetId = "a-test-button"; // we'll create a button with this ID. + +add_task(function setup() { + // create a XUL button and add it to the palette. + createDummyXULButton(kXULWidgetId, "test-button"); +}); + +add_task(function* customizeToolbarAndKeepIt() { + ok(gNavToolbox.toolbarset, "There should be a toolbarset"); + let toolbarID = "testAustralisCustomToolbar"; + gNavToolbox.appendCustomToolbar(toolbarID, ""); + let toolbarDOMID = getToolboxCustomToolbarId(toolbarID); + let toolbarElement = document.getElementById(toolbarDOMID); + ok(toolbarElement, "There should be a toolbar"); + if (!toolbarElement) { + ok(false, "No toolbar created, bailing out of the test."); + return; + } + is(toolbarElement.nextSibling, gNavToolbox.toolbarset, + "Toolbar should have been inserted in toolbox, before toolbarset element"); + let cuiAreaType = CustomizableUI.getAreaType(toolbarDOMID); + is(cuiAreaType, CustomizableUI.TYPE_TOOLBAR, + "CustomizableUI should know the area and think it's a toolbar"); + if (cuiAreaType != CustomizableUI.TYPE_TOOLBAR) { + ok(false, "Toolbar not registered successfully, bailing out of the test."); + toolbarElement.remove(); + return; + } + ok(!CustomizableUI.getWidgetIdsInArea(toolbarDOMID).length, "There should be no widgets in the area yet."); + CustomizableUI.addWidgetToArea("open-file-button", toolbarDOMID, 0); + ok(toolbarElement.hasChildNodes(), "Toolbar should now have a button."); + assertAreaPlacements(toolbarDOMID, ["open-file-button"]); + + gNavToolbox.toolbarset.setAttribute("toolbar1", toolbarID + ":open-file-button"); + document.persist(gNavToolbox.toolbarset.id, "toolbar1"); + + yield startCustomizing(); + // First, exit customize mode without doing anything, and verify the toolbar doesn't get removed. + yield endCustomizing(); + ok(!CustomizableUI.inDefaultState, "Shouldn't be in default state, the toolbar should still be there."); + cuiAreaType = CustomizableUI.getAreaType(toolbarDOMID); + is(cuiAreaType, CustomizableUI.TYPE_TOOLBAR, + "CustomizableUI should still know the area and think it's a toolbar"); + ok(toolbarElement.parentNode, "Toolbar should still be in the DOM."); + ok(toolbarElement.hasChildNodes(), "Toolbar should still have items in it."); + assertAreaPlacements(toolbarDOMID, ["open-file-button"]); + + let newWindow = yield openAndLoadWindow({}, true); + is(newWindow.gNavToolbox.toolbarset.getAttribute("toolbar1"), + gNavToolbox.toolbarset.getAttribute("toolbar1"), + "Attribute should be the same in new window"); + yield promiseWindowClosed(newWindow); + + // Then customize again, and this time empty out the toolbar and verify it *does* get removed. + yield startCustomizing(); + let openFileButton = document.getElementById("open-file-button"); + let palette = document.getElementById("customization-palette"); + simulateItemDrag(openFileButton, palette); + ok(!CustomizableUI.inDefaultState, "Shouldn't be in default state because there's still a non-collapsed toolbar."); + ok(!toolbarElement.hasChildNodes(), "Toolbar should have no more child nodes."); + + toolbarElement.collapsed = true; + ok(CustomizableUI.inDefaultState, "Should be in default state because there's now just a collapsed toolbar."); + toolbarElement.collapsed = false; + ok(!CustomizableUI.inDefaultState, "Shouldn't be in default state because there's a non-collapsed toolbar again."); + yield endCustomizing(); + ok(CustomizableUI.inDefaultState, "Should be in default state because the toolbar should have been removed."); + + newWindow = yield openAndLoadWindow({}, true); + ok(!newWindow.gNavToolbox.toolbarset.hasAttribute("toolbar1"), + "Attribute should be gone in new window"); + yield promiseWindowClosed(newWindow); + + ok(!toolbarElement.parentNode, "Toolbar should no longer be in the DOM."); + cuiAreaType = CustomizableUI.getAreaType(toolbarDOMID); + is(cuiAreaType, null, "CustomizableUI should have forgotten all about the area"); +}); + +add_task(function* resetShouldDealWithCustomToolbars() { + ok(gNavToolbox.toolbarset, "There should be a toolbarset"); + let toolbarID = "testAustralisCustomToolbar"; + gNavToolbox.appendCustomToolbar(toolbarID, ""); + let toolbarDOMID = getToolboxCustomToolbarId(toolbarID); + let toolbarElement = document.getElementById(toolbarDOMID); + ok(toolbarElement, "There should be a toolbar"); + if (!toolbarElement) { + ok(false, "No toolbar created, bailing out of the test."); + return; + } + is(toolbarElement.nextSibling, gNavToolbox.toolbarset, + "Toolbar should have been inserted in toolbox, before toolbarset element"); + let cuiAreaType = CustomizableUI.getAreaType(toolbarDOMID); + is(cuiAreaType, CustomizableUI.TYPE_TOOLBAR, + "CustomizableUI should know the area and think it's a toolbar"); + if (cuiAreaType != CustomizableUI.TYPE_TOOLBAR) { + ok(false, "Toolbar not registered successfully, bailing out of the test."); + toolbarElement.remove(); + return; + } + ok(!CustomizableUI.getWidgetIdsInArea(toolbarDOMID).length, "There should be no widgets in the area yet."); + CustomizableUI.addWidgetToArea(kXULWidgetId, toolbarDOMID, 0); + ok(toolbarElement.hasChildNodes(), "Toolbar should now have a button."); + assertAreaPlacements(toolbarDOMID, [kXULWidgetId]); + + gNavToolbox.toolbarset.setAttribute("toolbar2", `${toolbarID}:${kXULWidgetId}`); + document.persist(gNavToolbox.toolbarset.id, "toolbar2"); + + let newWindow = yield openAndLoadWindow({}, true); + is(newWindow.gNavToolbox.toolbarset.getAttribute("toolbar2"), + gNavToolbox.toolbarset.getAttribute("toolbar2"), + "Attribute should be the same in new window"); + yield promiseWindowClosed(newWindow); + + CustomizableUI.reset(); + + newWindow = yield openAndLoadWindow({}, true); + ok(!newWindow.gNavToolbox.toolbarset.hasAttribute("toolbar2"), + "Attribute should be gone in new window"); + yield promiseWindowClosed(newWindow); + + ok(CustomizableUI.inDefaultState, "Should be in default state after reset."); + let xulButton = document.getElementById(kXULWidgetId); + ok(!xulButton, "XUL button shouldn't be in the document anymore."); + ok(gNavToolbox.palette.querySelector(`#${kXULWidgetId}`), "XUL button should be in the palette"); + ok(!toolbarElement.hasChildNodes(), "Toolbar should have no more child nodes."); + ok(!toolbarElement.parentNode, "Toolbar should no longer be in the DOM."); + cuiAreaType = CustomizableUI.getAreaType(toolbarDOMID); + is(cuiAreaType, null, "CustomizableUI should have forgotten all about the area"); +}); + + +add_task(function*() { + let newWin = yield openAndLoadWindow({}, true); + ok(!newWin.gNavToolbox.toolbarset.hasAttribute("toolbar1"), "New window shouldn't have attribute toolbar1"); + ok(!newWin.gNavToolbox.toolbarset.hasAttribute("toolbar2"), "New window shouldn't have attribute toolbar2"); + yield promiseWindowClosed(newWin); +}); diff --git a/browser/components/customizableui/test/browser_976792_insertNodeInWindow.js b/browser/components/customizableui/test/browser_976792_insertNodeInWindow.js new file mode 100644 index 000000000..3bfa8c25d --- /dev/null +++ b/browser/components/customizableui/test/browser_976792_insertNodeInWindow.js @@ -0,0 +1,414 @@ +/* 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 kToolbarName = "test-insertNodeInWindow-placements-toolbar"; +const kTestWidgetPrefix = "test-widget-for-insertNodeInWindow-placements-"; + + +/* +Tries to replicate the situation of having a placement list like this: + +exists-1,trying-to-insert-this,doesn't-exist,exists-2 +*/ +add_task(function*() { + let testWidgetExists = [true, false, false, true]; + let widgetIds = []; + for (let i = 0; i < testWidgetExists.length; i++) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + if (testWidgetExists[i]) { + let spec = {id: id, type: "button", removable: true, label: "test", tooltiptext: "" + i}; + CustomizableUI.createWidget(spec); + } + } + + let toolbarNode = createToolbarWithPlacements(kToolbarName, widgetIds); + assertAreaPlacements(kToolbarName, widgetIds); + + let btnId = kTestWidgetPrefix + 1; + let btn = createDummyXULButton(btnId, "test"); + CustomizableUI.ensureWidgetPlacedInWindow(btnId, window); + + is(btn.parentNode.id, kToolbarName, "New XUL widget should be placed inside new toolbar"); + + is(btn.previousSibling.id, toolbarNode.firstChild.id, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements"); + + widgetIds.forEach(id => CustomizableUI.destroyWidget(id)); + btn.remove(); + removeCustomToolbars(); + yield resetCustomization(); +}); + + +/* +Tests nodes get placed inside the toolbar's overflow as expected. Replicates a +situation similar to: + +exists-1,exists-2,overflow-1,trying-to-insert-this,overflow-2 +*/ +add_task(function*() { + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + + let widgetIds = []; + for (let i = 0; i < 5; i++) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + let spec = {id: id, type: "button", removable: true, label: "insertNodeInWindow test", tooltiptext: "" + i}; + CustomizableUI.createWidget(spec); + CustomizableUI.addWidgetToArea(id, "nav-bar"); + } + + for (let id of widgetIds) { + document.getElementById(id).style.minWidth = "200px"; + } + + let originalWindowWidth = window.outerWidth; + window.resizeTo(400, window.outerHeight); + yield waitForCondition(() => navbar.hasAttribute("overflowing")); + + let testWidgetId = kTestWidgetPrefix + 3; + + CustomizableUI.destroyWidget(testWidgetId); + + let btn = createDummyXULButton(testWidgetId, "test"); + CustomizableUI.ensureWidgetPlacedInWindow(testWidgetId, window); + + is(btn.parentNode.id, navbar.overflowable._list.id, "New XUL widget should be placed inside overflow of toolbar"); + is(btn.previousSibling.id, kTestWidgetPrefix + 2, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements"); + is(btn.nextSibling.id, kTestWidgetPrefix + 4, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements"); + + window.resizeTo(originalWindowWidth, window.outerHeight); + + widgetIds.forEach(id => CustomizableUI.destroyWidget(id)); + CustomizableUI.removeWidgetFromArea(btn.id, kToolbarName); + btn.remove(); + yield resetCustomization(); + yield waitForCondition(() => !navbar.hasAttribute("overflowing")); +}); + + +/* +Tests nodes get placed inside the toolbar's overflow as expected. Replicates a +placements situation similar to: + +exists-1,exists-2,overflow-1,doesn't-exist,trying-to-insert-this,overflow-2 +*/ +add_task(function*() { + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + + let widgetIds = []; + for (let i = 0; i < 5; i++) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + let spec = {id: id, type: "button", removable: true, label: "insertNodeInWindow test", tooltiptext: "" + i}; + CustomizableUI.createWidget(spec); + CustomizableUI.addWidgetToArea(id, "nav-bar"); + } + + for (let id of widgetIds) { + document.getElementById(id).style.minWidth = "200px"; + } + + let originalWindowWidth = window.outerWidth; + window.resizeTo(400, window.outerHeight); + yield waitForCondition(() => navbar.hasAttribute("overflowing")); + + let testWidgetId = kTestWidgetPrefix + 3; + + CustomizableUI.destroyWidget(kTestWidgetPrefix + 2); + CustomizableUI.destroyWidget(testWidgetId); + + let btn = createDummyXULButton(testWidgetId, "test"); + CustomizableUI.ensureWidgetPlacedInWindow(testWidgetId, window); + + is(btn.parentNode.id, navbar.overflowable._list.id, "New XUL widget should be placed inside overflow of toolbar"); + is(btn.previousSibling.id, kTestWidgetPrefix + 1, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements"); + is(btn.nextSibling.id, kTestWidgetPrefix + 4, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements"); + + window.resizeTo(originalWindowWidth, window.outerHeight); + + widgetIds.forEach(id => CustomizableUI.destroyWidget(id)); + CustomizableUI.removeWidgetFromArea(btn.id, kToolbarName); + btn.remove(); + yield resetCustomization(); + yield waitForCondition(() => !navbar.hasAttribute("overflowing")); +}); + + +/* +Tests nodes get placed inside the toolbar's overflow as expected. Replicates a +placements situation similar to: + +exists-1,exists-2,overflow-1,doesn't-exist,trying-to-insert-this,doesn't-exist +*/ +add_task(function*() { + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + + let widgetIds = []; + for (let i = 0; i < 5; i++) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + let spec = {id: id, type: "button", removable: true, label: "insertNodeInWindow test", tooltiptext: "" + i}; + CustomizableUI.createWidget(spec); + CustomizableUI.addWidgetToArea(id, "nav-bar"); + } + + for (let id of widgetIds) { + document.getElementById(id).style.minWidth = "200px"; + } + + let originalWindowWidth = window.outerWidth; + window.resizeTo(400, window.outerHeight); + yield waitForCondition(() => navbar.hasAttribute("overflowing")); + + let testWidgetId = kTestWidgetPrefix + 3; + + CustomizableUI.destroyWidget(kTestWidgetPrefix + 2); + CustomizableUI.destroyWidget(testWidgetId); + CustomizableUI.destroyWidget(kTestWidgetPrefix + 4); + + let btn = createDummyXULButton(testWidgetId, "test"); + CustomizableUI.ensureWidgetPlacedInWindow(testWidgetId, window); + + is(btn.parentNode.id, navbar.overflowable._list.id, "New XUL widget should be placed inside overflow of toolbar"); + is(btn.previousSibling.id, kTestWidgetPrefix + 1, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements"); + is(btn.nextSibling, null, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements"); + + window.resizeTo(originalWindowWidth, window.outerHeight); + + widgetIds.forEach(id => CustomizableUI.destroyWidget(id)); + CustomizableUI.removeWidgetFromArea(btn.id, kToolbarName); + btn.remove(); + yield resetCustomization(); + yield waitForCondition(() => !navbar.hasAttribute("overflowing")); +}); + + +/* +Tests nodes get placed inside the toolbar's overflow as expected. Replicates a +placements situation similar to: + +exists-1,exists-2,overflow-1,can't-overflow,trying-to-insert-this,overflow-2 +*/ +add_task(function*() { + let navbar = document.getElementById(CustomizableUI.AREA_NAVBAR); + + let widgetIds = []; + for (let i = 5; i >= 0; i--) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + let spec = {id: id, type: "button", removable: true, label: "insertNodeInWindow test", tooltiptext: "" + i}; + CustomizableUI.createWidget(spec); + CustomizableUI.addWidgetToArea(id, "nav-bar", 0); + } + + for (let i = 10; i < 15; i++) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + let spec = {id: id, type: "button", removable: true, label: "insertNodeInWindow test", tooltiptext: "" + i}; + CustomizableUI.createWidget(spec); + CustomizableUI.addWidgetToArea(id, "nav-bar"); + } + + for (let id of widgetIds) { + document.getElementById(id).style.minWidth = "200px"; + } + + let originalWindowWidth = window.outerWidth; + window.resizeTo(400, window.outerHeight); + yield waitForCondition(() => navbar.hasAttribute("overflowing")); + + // Find last widget that doesn't allow overflowing + let nonOverflowing = navbar.customizationTarget.lastChild; + is(nonOverflowing.getAttribute("overflows"), "false", "Last child is expected to not allow overflowing"); + isnot(nonOverflowing.getAttribute("skipintoolbarset"), "true", "Last child is expected to not be skipintoolbarset"); + + let testWidgetId = kTestWidgetPrefix + 10; + CustomizableUI.destroyWidget(testWidgetId); + + let btn = createDummyXULButton(testWidgetId, "test"); + CustomizableUI.ensureWidgetPlacedInWindow(testWidgetId, window); + + is(btn.parentNode.id, navbar.overflowable._list.id, "New XUL widget should be placed inside overflow of toolbar"); + is(btn.nextSibling.id, kTestWidgetPrefix + 11, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements"); + + window.resizeTo(originalWindowWidth, window.outerHeight); + + widgetIds.forEach(id => CustomizableUI.destroyWidget(id)); + CustomizableUI.removeWidgetFromArea(btn.id, kToolbarName); + btn.remove(); + yield resetCustomization(); + yield waitForCondition(() => !navbar.hasAttribute("overflowing")); +}); + + +/* +Tests nodes get placed inside the toolbar's overflow as expected. Replicates a +placements situation similar to: + +exists-1,exists-2,overflow-1,trying-to-insert-this,can't-overflow,overflow-2 +*/ +add_task(function*() { + let widgetIds = []; + let missingId = 2; + let nonOverflowableId = 3; + for (let i = 0; i < 5; i++) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + if (i != missingId) { + // Setting min-width to make the overflow state not depend on styling of the button and/or + // screen width + let spec = {id: id, type: "button", removable: true, label: "test", tooltiptext: "" + i, + onCreated: function(node) { + node.style.minWidth = "200px"; + if (id == (kTestWidgetPrefix + nonOverflowableId)) { + node.setAttribute("overflows", false); + } + }}; + info("Creating: " + id); + CustomizableUI.createWidget(spec); + } + } + + let toolbarNode = createOverflowableToolbarWithPlacements(kToolbarName, widgetIds); + assertAreaPlacements(kToolbarName, widgetIds); + ok(!toolbarNode.hasAttribute("overflowing"), "Toolbar shouldn't overflow to start with."); + + let originalWindowWidth = window.outerWidth; + window.resizeTo(400, window.outerHeight); + yield waitForCondition(() => toolbarNode.hasAttribute("overflowing")); + ok(toolbarNode.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + + let btnId = kTestWidgetPrefix + missingId; + let btn = createDummyXULButton(btnId, "test"); + CustomizableUI.ensureWidgetPlacedInWindow(btnId, window); + + is(btn.parentNode.id, kToolbarName + "-overflow-list", "New XUL widget should be placed inside new toolbar's overflow"); + is(btn.previousSibling.id, kTestWidgetPrefix + 1, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements"); + is(btn.nextSibling.id, kTestWidgetPrefix + 4, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements"); + + window.resizeTo(originalWindowWidth, window.outerHeight); + yield waitForCondition(() => !toolbarNode.hasAttribute("overflowing")); + + btn.remove(); + widgetIds.forEach(id => CustomizableUI.destroyWidget(id)); + removeCustomToolbars(); + yield resetCustomization(); +}); + + +/* +Tests nodes do *not* get placed in the toolbar's overflow. Replicates a +plcements situation similar to: + +exists-1,trying-to-insert-this,exists-2,overflowed-1 +*/ +add_task(function*() { + let widgetIds = []; + let missingId = 1; + for (let i = 0; i < 5; i++) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + if (i != missingId) { + // Setting min-width to make the overflow state not depend on styling of the button and/or + // screen width + let spec = {id: id, type: "button", removable: true, label: "test", tooltiptext: "" + i, + onCreated: function(node) { node.style.minWidth = "100px"; }}; + info("Creating: " + id); + CustomizableUI.createWidget(spec); + } + } + + let toolbarNode = createOverflowableToolbarWithPlacements(kToolbarName, widgetIds); + assertAreaPlacements(kToolbarName, widgetIds); + ok(!toolbarNode.hasAttribute("overflowing"), "Toolbar shouldn't overflow to start with."); + + let originalWindowWidth = window.outerWidth; + window.resizeTo(400, window.outerHeight); + yield waitForCondition(() => toolbarNode.hasAttribute("overflowing")); + ok(toolbarNode.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + + let btnId = kTestWidgetPrefix + missingId; + let btn = createDummyXULButton(btnId, "test"); + CustomizableUI.ensureWidgetPlacedInWindow(btnId, window); + + is(btn.parentNode.id, kToolbarName + "-target", "New XUL widget should be placed inside new toolbar"); + + window.resizeTo(originalWindowWidth, window.outerHeight); + yield waitForCondition(() => !toolbarNode.hasAttribute("overflowing")); + + btn.remove(); + widgetIds.forEach(id => CustomizableUI.destroyWidget(id)); + removeCustomToolbars(); + yield resetCustomization(); +}); + + +/* +Tests inserting a node onto the end of an overflowing toolbar *doesn't* put it in +the overflow list when the widget disallows overflowing. ie: + +exists-1,exists-2,overflows-1,trying-to-insert-this + +Where trying-to-insert-this has overflows=false +*/ +add_task(function*() { + let widgetIds = []; + let missingId = 3; + for (let i = 0; i < 5; i++) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + if (i != missingId) { + // Setting min-width to make the overflow state not depend on styling of the button and/or + // screen width + let spec = {id: id, type: "button", removable: true, label: "test", tooltiptext: "" + i, + onCreated: function(node) { node.style.minWidth = "200px"; }}; + info("Creating: " + id); + CustomizableUI.createWidget(spec); + } + } + + let toolbarNode = createOverflowableToolbarWithPlacements(kToolbarName, widgetIds); + assertAreaPlacements(kToolbarName, widgetIds); + ok(!toolbarNode.hasAttribute("overflowing"), "Toolbar shouldn't overflow to start with."); + + let originalWindowWidth = window.outerWidth; + window.resizeTo(400, window.outerHeight); + yield waitForCondition(() => toolbarNode.hasAttribute("overflowing")); + ok(toolbarNode.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + + let btnId = kTestWidgetPrefix + missingId; + let btn = createDummyXULButton(btnId, "test"); + btn.setAttribute("overflows", false); + CustomizableUI.ensureWidgetPlacedInWindow(btnId, window); + + is(btn.parentNode.id, kToolbarName + "-target", "New XUL widget should be placed inside new toolbar"); + is(btn.nextSibling, null, + "insertNodeInWindow should have placed new XUL widget in correct place in DOM according to placements"); + + window.resizeTo(originalWindowWidth, window.outerHeight); + yield waitForCondition(() => !toolbarNode.hasAttribute("overflowing")); + + btn.remove(); + widgetIds.forEach(id => CustomizableUI.destroyWidget(id)); + removeCustomToolbars(); + yield resetCustomization(); +}); + + +add_task(function* asyncCleanUp() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_978084_dragEnd_after_move.js b/browser/components/customizableui/test/browser_978084_dragEnd_after_move.js new file mode 100644 index 000000000..a653c2d51 --- /dev/null +++ b/browser/components/customizableui/test/browser_978084_dragEnd_after_move.js @@ -0,0 +1,46 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +var draggedItem; + +/** + * Check that customizing-movingItem gets removed on a drop when the item is moved. + */ + +// Drop on the palette +add_task(function*() { + draggedItem = document.createElement("toolbarbutton"); + draggedItem.id = "test-dragEnd-after-move1"; + draggedItem.setAttribute("label", "Test"); + draggedItem.setAttribute("removable", "true"); + let navbar = document.getElementById("nav-bar"); + navbar.customizationTarget.appendChild(draggedItem); + yield startCustomizing(); + simulateItemDrag(draggedItem, gCustomizeMode.visiblePalette); + is(document.documentElement.hasAttribute("customizing-movingItem"), false, + "Make sure customizing-movingItem is removed after dragging to the palette"); + yield endCustomizing(); +}); + +// Drop on a customization target itself +add_task(function*() { + draggedItem = document.createElement("toolbarbutton"); + draggedItem.id = "test-dragEnd-after-move2"; + draggedItem.setAttribute("label", "Test"); + draggedItem.setAttribute("removable", "true"); + let dest = createToolbarWithPlacements("test-dragEnd"); + let navbar = document.getElementById("nav-bar"); + navbar.customizationTarget.appendChild(draggedItem); + yield startCustomizing(); + simulateItemDrag(draggedItem, dest.customizationTarget); + is(document.documentElement.hasAttribute("customizing-movingItem"), false, + "Make sure customizing-movingItem is removed"); + yield endCustomizing(); +}); + +add_task(function* asyncCleanup() { + yield endCustomizing(); + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_980155_add_overflow_toolbar.js b/browser/components/customizableui/test/browser_980155_add_overflow_toolbar.js new file mode 100644 index 000000000..15197ac86 --- /dev/null +++ b/browser/components/customizableui/test/browser_980155_add_overflow_toolbar.js @@ -0,0 +1,51 @@ +/* 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 kToolbarName = "test-new-overflowable-toolbar"; +const kTestWidgetPrefix = "test-widget-for-overflowable-toolbar-"; + +add_task(function* addOverflowingToolbar() { + let originalWindowWidth = window.outerWidth; + + let widgetIds = []; + for (let i = 0; i < 10; i++) { + let id = kTestWidgetPrefix + i; + widgetIds.push(id); + let spec = {id: id, type: "button", removable: true, label: "test", tooltiptext: "" + i}; + CustomizableUI.createWidget(spec); + } + + let toolbarNode = createOverflowableToolbarWithPlacements(kToolbarName, widgetIds); + assertAreaPlacements(kToolbarName, widgetIds); + + for (let id of widgetIds) { + document.getElementById(id).style.minWidth = "200px"; + } + + isnot(toolbarNode.overflowable, null, "Toolbar should have overflowable controller"); + isnot(toolbarNode.customizationTarget, null, "Toolbar should have customization target"); + isnot(toolbarNode.customizationTarget, toolbarNode, "Customization target should not be toolbar node"); + + let oldChildCount = toolbarNode.customizationTarget.childElementCount; + let overflowableList = document.getElementById(kToolbarName + "-overflow-list"); + let oldOverflowCount = overflowableList.childElementCount; + + isnot(oldChildCount, 0, "Toolbar should have non-overflowing widgets"); + + window.resizeTo(400, window.outerHeight); + yield waitForCondition(() => toolbarNode.hasAttribute("overflowing")); + ok(toolbarNode.hasAttribute("overflowing"), "Should have an overflowing toolbar."); + ok(toolbarNode.customizationTarget.childElementCount < oldChildCount, "Should have fewer children."); + ok(overflowableList.childElementCount > oldOverflowCount, "Should have more overflowed widgets."); + + window.resizeTo(originalWindowWidth, window.outerHeight); +}); + + +add_task(function* asyncCleanup() { + removeCustomToolbars(); + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_981305_separator_insertion.js b/browser/components/customizableui/test/browser_981305_separator_insertion.js new file mode 100644 index 000000000..8d4d86c2a --- /dev/null +++ b/browser/components/customizableui/test/browser_981305_separator_insertion.js @@ -0,0 +1,73 @@ +/* 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"; + +var tempElements = []; + +function insertTempItemsIntoMenu(parentMenu) { + // Last element is null to insert at the end: + let beforeEls = [parentMenu.firstChild, parentMenu.lastChild, null]; + for (let i = 0; i < beforeEls.length; i++) { + let sep = document.createElement("menuseparator"); + tempElements.push(sep); + parentMenu.insertBefore(sep, beforeEls[i]); + let menu = document.createElement("menu"); + tempElements.push(menu); + parentMenu.insertBefore(menu, beforeEls[i]); + // And another separator for good measure: + sep = document.createElement("menuseparator"); + tempElements.push(sep); + parentMenu.insertBefore(sep, beforeEls[i]); + } +} + +function checkSeparatorInsertion(menuId, buttonId, subviewId) { + return function*() { + info("Checking for duplicate separators in " + buttonId + " widget"); + let menu = document.getElementById(menuId); + insertTempItemsIntoMenu(menu); + + let placement = CustomizableUI.getPlacementOfWidget(buttonId); + let changedPlacement = false; + if (!placement || placement.area != CustomizableUI.AREA_PANEL) { + CustomizableUI.addWidgetToArea(buttonId, CustomizableUI.AREA_PANEL); + changedPlacement = true; + } + yield PanelUI.show(); + + let button = document.getElementById(buttonId); + button.click(); + + yield waitForCondition(() => !PanelUI.multiView.hasAttribute("transitioning")); + let subview = document.getElementById(subviewId); + ok(subview.firstChild, "Subview should have a kid"); + is(subview.firstChild.localName, "toolbarbutton", "There should be no separators to start with"); + + for (let kid of subview.children) { + if (kid.localName == "menuseparator") { + ok(kid.previousSibling && kid.previousSibling.localName != "menuseparator", + "Separators should never have another separator next to them, and should never be the first node."); + } + } + + let panelHiddenPromise = promisePanelHidden(window); + PanelUI.hide(); + yield panelHiddenPromise; + + if (changedPlacement) { + CustomizableUI.reset(); + } + }; +} + +add_task(checkSeparatorInsertion("menuWebDeveloperPopup", "developer-button", "PanelUI-developerItems")); +add_task(checkSeparatorInsertion("viewSidebarMenu", "sidebar-button", "PanelUI-sidebarItems")); + +registerCleanupFunction(function() { + for (let el of tempElements) { + el.remove(); + } + tempElements = null; +}); diff --git a/browser/components/customizableui/test/browser_981418-widget-onbeforecreated-handler.js b/browser/components/customizableui/test/browser_981418-widget-onbeforecreated-handler.js new file mode 100644 index 000000000..9a7227a47 --- /dev/null +++ b/browser/components/customizableui/test/browser_981418-widget-onbeforecreated-handler.js @@ -0,0 +1,93 @@ +/* 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 kWidgetId = 'test-981418-widget-onbeforecreated'; + +// Should be able to add broken view widget +add_task(function* testAddOnBeforeCreatedWidget() { + let viewShownDeferred = Promise.defer(); + let onBeforeCreatedCalled = false; + let widgetSpec = { + id: kWidgetId, + type: 'view', + viewId: kWidgetId + 'idontexistyet', + onBeforeCreated: function(doc) { + let view = doc.createElement("panelview"); + view.id = kWidgetId + 'idontexistyet'; + let label = doc.createElement("label"); + label.setAttribute("value", "Hello world"); + label.className = 'panel-subview-header'; + view.appendChild(label); + document.getElementById("PanelUI-multiView").appendChild(view); + onBeforeCreatedCalled = true; + }, + onViewShowing: function() { + viewShownDeferred.resolve(); + } + }; + + let noError = true; + try { + CustomizableUI.createWidget(widgetSpec); + CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_NAVBAR); + } catch (ex) { + Cu.reportError(ex); + noError = false; + } + ok(noError, "Should not throw an exception trying to add the widget."); + ok(onBeforeCreatedCalled, "onBeforeCreated should have been called"); + + let widgetNode = document.getElementById(kWidgetId); + ok(widgetNode, "Widget should exist"); + if (widgetNode) { + try { + widgetNode.click(); + + let tempPanel = document.getElementById("customizationui-widget-panel"); + let panelShownPromise = promisePanelElementShown(window, tempPanel); + + let shownTimeout = setTimeout(() => viewShownDeferred.reject("Panel not shown within 20s"), 20000); + yield viewShownDeferred.promise; + yield panelShownPromise; + clearTimeout(shownTimeout); + ok(true, "Found view shown"); + + let panelHiddenPromise = promisePanelElementHidden(window, tempPanel); + tempPanel.hidePopup(); + yield panelHiddenPromise; + + CustomizableUI.addWidgetToArea(kWidgetId, CustomizableUI.AREA_PANEL); + yield PanelUI.show(); + + viewShownDeferred = Promise.defer(); + widgetNode.click(); + + shownTimeout = setTimeout(() => viewShownDeferred.reject("Panel not shown within 20s"), 20000); + yield viewShownDeferred.promise; + clearTimeout(shownTimeout); + ok(true, "Found view shown"); + + let panelHidden = promisePanelHidden(window); + PanelUI.hide(); + yield panelHidden; + } catch (ex) { + ok(false, "Unexpected exception (like a timeout for one of the yields) " + + "when testing view widget."); + } + } + + noError = true; + try { + CustomizableUI.destroyWidget(kWidgetId); + } catch (ex) { + Cu.reportError(ex); + noError = false; + } + ok(noError, "Should not throw an exception trying to remove the broken view widget."); +}); + +add_task(function* asyncCleanup() { + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_982656_restore_defaults_builtin_widgets.js b/browser/components/customizableui/test/browser_982656_restore_defaults_builtin_widgets.js new file mode 100644 index 000000000..e7f8d0cf4 --- /dev/null +++ b/browser/components/customizableui/test/browser_982656_restore_defaults_builtin_widgets.js @@ -0,0 +1,57 @@ +/* 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"; + +// Restoring default should not place addon widgets back in the toolbar +add_task(function*() { + ok(CustomizableUI.inDefaultState, "Default state to begin"); + + const kWidgetId = "bug982656-add-on-widget-should-not-restore-to-default-area"; + let widgetSpec = { + id: kWidgetId, + defaultArea: CustomizableUI.AREA_NAVBAR + }; + CustomizableUI.createWidget(widgetSpec); + + ok(!CustomizableUI.inDefaultState, "Not in default state after widget added"); + is(CustomizableUI.getPlacementOfWidget(kWidgetId).area, CustomizableUI.AREA_NAVBAR, "Widget should be in navbar"); + + yield resetCustomization(); + + ok(CustomizableUI.inDefaultState, "Back in default state after reset"); + is(CustomizableUI.getPlacementOfWidget(kWidgetId), null, "Widget now in palette"); + CustomizableUI.destroyWidget(kWidgetId); +}); + + +// resetCustomization shouldn't move 3rd party widgets out of custom toolbars +add_task(function*() { + const kToolbarId = "bug982656-toolbar-with-defaultset"; + const kWidgetId = "bug982656-add-on-widget-should-restore-to-default-area-when-area-is-not-builtin"; + ok(CustomizableUI.inDefaultState, "Everything should be in its default state."); + let toolbar = createToolbarWithPlacements(kToolbarId); + ok(CustomizableUI.areas.indexOf(kToolbarId) != -1, + "Toolbar has been registered."); + is(CustomizableUI.getAreaType(kToolbarId), CustomizableUI.TYPE_TOOLBAR, + "Area should be registered as toolbar"); + + let widgetSpec = { + id: kWidgetId, + defaultArea: kToolbarId + }; + CustomizableUI.createWidget(widgetSpec); + + ok(!CustomizableUI.inDefaultState, "No longer in default state after toolbar is registered and visible."); + is(CustomizableUI.getPlacementOfWidget(kWidgetId).area, kToolbarId, "Widget should be in custom toolbar"); + + yield resetCustomization(); + ok(CustomizableUI.inDefaultState, "Back in default state after reset"); + is(CustomizableUI.getPlacementOfWidget(kWidgetId).area, kToolbarId, "Widget still in custom toolbar"); + ok(toolbar.collapsed, "Custom toolbar should be collapsed after reset"); + + toolbar.remove(); + CustomizableUI.destroyWidget(kWidgetId); + CustomizableUI.unregisterArea(kToolbarId); +}); diff --git a/browser/components/customizableui/test/browser_984455_bookmarks_items_reparenting.js b/browser/components/customizableui/test/browser_984455_bookmarks_items_reparenting.js new file mode 100644 index 000000000..42b346c10 --- /dev/null +++ b/browser/components/customizableui/test/browser_984455_bookmarks_items_reparenting.js @@ -0,0 +1,267 @@ +/* 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"; + +var gNavBar = document.getElementById(CustomizableUI.AREA_NAVBAR); +var gOverflowList = document.getElementById(gNavBar.getAttribute("overflowtarget")); + +const kBookmarksButton = "bookmarks-menu-button"; +const kBookmarksItems = "personal-bookmarks"; +const kOriginalWindowWidth = window.outerWidth; +const kSmallWidth = 400; + +/** + * Helper function that opens the bookmarks menu, and returns a Promise that + * resolves as soon as the menu is ready for interaction. + */ +function bookmarksMenuPanelShown() { + let deferred = Promise.defer(); + let bookmarksMenuPopup = document.getElementById("BMB_bookmarksPopup"); + let onTransitionEnd = (e) => { + if (e.target == bookmarksMenuPopup) { + bookmarksMenuPopup.removeEventListener("transitionend", onTransitionEnd); + deferred.resolve(); + } + } + bookmarksMenuPopup.addEventListener("transitionend", onTransitionEnd); + return deferred.promise; +} + +/** + * Checks that the placesContext menu is correctly attached to the + * controller of some view. Returns a Promise that resolves as soon + * as the context menu is closed. + * + * @param aItemWithContextMenu the item that we need to synthesize hte + * right click on in order to open the context menu. + */ +function checkPlacesContextMenu(aItemWithContextMenu) { + return Task.spawn(function* () { + let contextMenu = document.getElementById("placesContext"); + let newBookmarkItem = document.getElementById("placesContext_new:bookmark"); + info("Waiting for context menu on " + aItemWithContextMenu.id); + let shownPromise = popupShown(contextMenu); + EventUtils.synthesizeMouseAtCenter(aItemWithContextMenu, + {type: "contextmenu", button: 2}); + yield shownPromise; + + ok(!newBookmarkItem.hasAttribute("disabled"), + "New bookmark item shouldn't be disabled"); + + info("Closing context menu"); + yield closePopup(contextMenu); + }); +} + +/** + * Opens the bookmarks menu panel, and then opens each of the "special" + * submenus in that list. Then it checks that those submenu's context menus + * are properly hooked up to a controller. + */ +function checkSpecialContextMenus() { + return Task.spawn(function* () { + let bookmarksMenuButton = document.getElementById(kBookmarksButton); + let bookmarksMenuPopup = document.getElementById("BMB_bookmarksPopup"); + + const kSpecialItemIDs = { + "BMB_bookmarksToolbar": "BMB_bookmarksToolbarPopup", + "BMB_unsortedBookmarks": "BMB_unsortedBookmarksPopup", + }; + + // Open the bookmarks menu button context menus and ensure that + // they have the proper views attached. + let shownPromise = bookmarksMenuPanelShown(); + let dropmarker = document.getAnonymousElementByAttribute(bookmarksMenuButton, + "anonid", "dropmarker"); + EventUtils.synthesizeMouseAtCenter(dropmarker, {}); + info("Waiting for bookmarks menu popup to show after clicking dropmarker.") + yield shownPromise; + + for (let menuID in kSpecialItemIDs) { + let menuItem = document.getElementById(menuID); + let menuPopup = document.getElementById(kSpecialItemIDs[menuID]); + info("Waiting to open menu for " + menuID); + let shownPromise = popupShown(menuPopup); + menuPopup.openPopup(menuItem, null, 0, 0, false, false, null); + yield shownPromise; + + yield checkPlacesContextMenu(menuPopup); + info("Closing menu for " + menuID); + yield closePopup(menuPopup); + } + + info("Closing bookmarks menu"); + yield closePopup(bookmarksMenuPopup); + }); +} + +/** + * Closes a focused popup by simulating pressing the Escape key, + * and returns a Promise that resolves as soon as the popup is closed. + * + * @param aPopup the popup node to close. + */ +function closePopup(aPopup) { + let hiddenPromise = popupHidden(aPopup); + EventUtils.synthesizeKey("VK_ESCAPE", {}); + return hiddenPromise; +} + +/** + * Helper function that checks that the context menu of the + * bookmark toolbar items chevron popup is correctly hooked up + * to the controller of a view. + */ +function checkBookmarksItemsChevronContextMenu() { + return Task.spawn(function*() { + let chevronPopup = document.getElementById("PlacesChevronPopup"); + let shownPromise = popupShown(chevronPopup); + let chevron = document.getElementById("PlacesChevron"); + EventUtils.synthesizeMouseAtCenter(chevron, {}); + info("Waiting for bookmark toolbar item chevron popup to show"); + yield shownPromise; + yield waitForCondition(() => { + for (let child of chevronPopup.children) { + if (child.style.visibility != "hidden") + return true; + } + return false; + }); + yield checkPlacesContextMenu(chevronPopup); + info("Waiting for bookmark toolbar item chevron popup to close"); + yield closePopup(chevronPopup); + }); +} + +/** + * Forces the window to a width that causes the nav-bar to overflow + * its contents. Returns a Promise that resolves as soon as the + * overflowable nav-bar is showing its chevron. + */ +function overflowEverything() { + info("Waiting for overflow"); + window.resizeTo(kSmallWidth, window.outerHeight); + return waitForCondition(() => gNavBar.hasAttribute("overflowing")); +} + +/** + * Returns the window to its original size from the start of the test, + * and returns a Promise that resolves when the nav-bar is no longer + * overflowing. + */ +function stopOverflowing() { + info("Waiting until we stop overflowing"); + window.resizeTo(kOriginalWindowWidth, window.outerHeight); + return waitForCondition(() => !gNavBar.hasAttribute("overflowing")); +} + +/** + * Checks that an item with ID aID is overflowing in the nav-bar. + * + * @param aID the ID of the node to check for overflowingness. + */ +function checkOverflowing(aID) { + ok(!gNavBar.querySelector("#" + aID), + "Item with ID " + aID + " should no longer be in the gNavBar"); + let item = gOverflowList.querySelector("#" + aID); + ok(item, "Item with ID " + aID + " should be overflowing"); + is(item.getAttribute("overflowedItem"), "true", + "Item with ID " + aID + " should have overflowedItem attribute"); +} + +/** + * Checks that an item with ID aID is not overflowing in the nav-bar. + * + * @param aID the ID of hte node to check for non-overflowingness. + */ +function checkNotOverflowing(aID) { + ok(!gOverflowList.querySelector("#" + aID), + "Item with ID " + aID + " should no longer be overflowing"); + let item = gNavBar.querySelector("#" + aID); + ok(item, "Item with ID " + aID + " should be in the nav bar"); + ok(!item.hasAttribute("overflowedItem"), + "Item with ID " + aID + " should not have overflowedItem attribute"); +} + +/** + * Test that overflowing the bookmarks menu button doesn't break the + * context menus for the Unsorted and Bookmarks Toolbar menu items. + */ +add_task(function* testOverflowingBookmarksButtonContextMenu() { + ok(!gNavBar.hasAttribute("overflowing"), "Should start with a non-overflowing toolbar."); + ok(CustomizableUI.inDefaultState, "Should start in default state."); + + // Open the Unsorted and Bookmarks Toolbar context menus and ensure + // that they have views attached. + yield checkSpecialContextMenus(); + + yield overflowEverything(); + checkOverflowing(kBookmarksButton); + + yield stopOverflowing(); + checkNotOverflowing(kBookmarksButton); + + yield checkSpecialContextMenus(); +}); + +/** + * Test that the bookmarks toolbar items context menu still works if moved + * to the menu from the overflow panel, and then back to the toolbar. + */ +add_task(function* testOverflowingBookmarksItemsContextMenu() { + info("Ensuring panel is ready."); + yield PanelUI.ensureReady(); + + let bookmarksToolbarItems = document.getElementById(kBookmarksItems); + gCustomizeMode.addToToolbar(bookmarksToolbarItems); + yield checkPlacesContextMenu(bookmarksToolbarItems); + + yield overflowEverything(); + checkOverflowing(kBookmarksItems) + + gCustomizeMode.addToPanel(bookmarksToolbarItems); + + yield stopOverflowing(); + + gCustomizeMode.addToToolbar(bookmarksToolbarItems); + yield checkPlacesContextMenu(bookmarksToolbarItems); +}); + +/** + * Test that overflowing the bookmarks toolbar items doesn't cause the + * context menu in the bookmarks toolbar items chevron to stop working. + */ +add_task(function* testOverflowingBookmarksItemsChevronContextMenu() { + // If it's not already there, let's move the bookmarks toolbar items to + // the nav-bar. + let bookmarksToolbarItems = document.getElementById(kBookmarksItems); + gCustomizeMode.addToToolbar(bookmarksToolbarItems); + + // We make the PlacesToolbarItems element be super tiny in order to force + // the bookmarks toolbar items into overflowing and making the chevron + // show itself. + let placesToolbarItems = document.getElementById("PlacesToolbarItems"); + let placesChevron = document.getElementById("PlacesChevron"); + placesToolbarItems.style.maxWidth = "10px"; + info("Waiting for chevron to no longer be collapsed"); + yield waitForCondition(() => !placesChevron.collapsed); + + yield checkBookmarksItemsChevronContextMenu(); + + yield overflowEverything(); + checkOverflowing(kBookmarksItems); + + yield stopOverflowing(); + checkNotOverflowing(kBookmarksItems); + + yield checkBookmarksItemsChevronContextMenu(); + + placesToolbarItems.style.removeProperty("max-width"); +}); + +add_task(function* asyncCleanup() { + window.resizeTo(kOriginalWindowWidth, window.outerHeight); + yield resetCustomization(); +}); diff --git a/browser/components/customizableui/test/browser_985815_propagate_setToolbarVisibility.js b/browser/components/customizableui/test/browser_985815_propagate_setToolbarVisibility.js new file mode 100644 index 000000000..c341c2158 --- /dev/null +++ b/browser/components/customizableui/test/browser_985815_propagate_setToolbarVisibility.js @@ -0,0 +1,45 @@ +/* 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"; + +add_task(function*() { + ok(CustomizableUI.inDefaultState, "Should start in default state."); + this.otherWin = yield openAndLoadWindow({private: true}, true); + yield startCustomizing(this.otherWin); + let resetButton = this.otherWin.document.getElementById("customization-reset-button"); + ok(resetButton.disabled, "Reset button should be disabled"); + + if (typeof CustomizableUI.setToolbarVisibility == "function") { + CustomizableUI.setToolbarVisibility("PersonalToolbar", true); + } else { + setToolbarVisibility(document.getElementById("PersonalToolbar"), true); + } + + let otherPersonalToolbar = this.otherWin.document.getElementById("PersonalToolbar"); + let personalToolbar = document.getElementById("PersonalToolbar"); + ok(!otherPersonalToolbar.collapsed, "Toolbar should be uncollapsed in private window"); + ok(!personalToolbar.collapsed, "Toolbar should be uncollapsed in normal window"); + ok(!resetButton.disabled, "Reset button should be enabled"); + + yield this.otherWin.gCustomizeMode.reset(); + + ok(otherPersonalToolbar.collapsed, "Toolbar should be collapsed in private window"); + ok(personalToolbar.collapsed, "Toolbar should be collapsed in normal window"); + ok(resetButton.disabled, "Reset button should be disabled"); + + yield endCustomizing(this.otherWin); + + yield promiseWindowClosed(this.otherWin); +}); + + +add_task(function* asyncCleanup() { + if (this.otherWin && !this.otherWin.closed) { + yield promiseWindowClosed(this.otherWin); + } + if (!CustomizableUI.inDefaultState) { + CustomizableUI.reset(); + } +}); diff --git a/browser/components/customizableui/test/browser_987177_destroyWidget_xul.js b/browser/components/customizableui/test/browser_987177_destroyWidget_xul.js new file mode 100644 index 000000000..6a4d0aab4 --- /dev/null +++ b/browser/components/customizableui/test/browser_987177_destroyWidget_xul.js @@ -0,0 +1,33 @@ +/* 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 BUTTONID = "test-XUL-wrapper-destroyWidget"; + + +add_task(function() { + let btn = createDummyXULButton(BUTTONID, "XUL btn"); + gNavToolbox.palette.appendChild(btn); + let firstWrapper = CustomizableUI.getWidget(BUTTONID).forWindow(window); + ok(firstWrapper, "Should get a wrapper"); + ok(firstWrapper.node, "Node should be there on first wrapper."); + + btn.remove(); + CustomizableUI.destroyWidget(BUTTONID); + let secondWrapper = CustomizableUI.getWidget(BUTTONID).forWindow(window); + isnot(firstWrapper, secondWrapper, "Wrappers should be different after destroyWidget call."); + ok(!firstWrapper.node, "No node should be there on old wrapper."); + ok(!secondWrapper.node, "No node should be there on new wrapper."); + + btn = createDummyXULButton(BUTTONID, "XUL btn"); + gNavToolbox.palette.appendChild(btn); + let thirdWrapper = CustomizableUI.getWidget(BUTTONID).forWindow(window); + ok(thirdWrapper, "Should get a wrapper"); + is(secondWrapper, thirdWrapper, "Should get the second wrapper again."); + ok(firstWrapper.node, "Node should be there on old wrapper."); + ok(secondWrapper.node, "Node should be there on second wrapper."); + ok(thirdWrapper.node, "Node should be there on third wrapper."); +}); + diff --git a/browser/components/customizableui/test/browser_987177_xul_wrapper_updating.js b/browser/components/customizableui/test/browser_987177_xul_wrapper_updating.js new file mode 100644 index 000000000..f838e204d --- /dev/null +++ b/browser/components/customizableui/test/browser_987177_xul_wrapper_updating.js @@ -0,0 +1,74 @@ +/* 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 BUTTONID = "test-XUL-wrapper-widget"; +add_task(function() { + let btn = createDummyXULButton(BUTTONID, "XUL btn"); + gNavToolbox.palette.appendChild(btn); + let groupWrapper = CustomizableUI.getWidget(BUTTONID); + ok(groupWrapper, "Should get a group wrapper"); + let singleWrapper = groupWrapper.forWindow(window); + ok(singleWrapper, "Should get a single wrapper"); + is(singleWrapper.node, btn, "Node should be in the wrapper"); + is(groupWrapper.instances.length, 1, "There should be 1 instance on the group wrapper"); + is(groupWrapper.instances[0].node, btn, "Button should be that instance."); + + CustomizableUI.addWidgetToArea(BUTTONID, CustomizableUI.AREA_NAVBAR); + + let otherSingleWrapper = groupWrapper.forWindow(window); + is(singleWrapper, otherSingleWrapper, "Should get the same wrapper after adding the node to the navbar."); + is(singleWrapper.node, btn, "Node should be in the wrapper"); + is(groupWrapper.instances.length, 1, "There should be 1 instance on the group wrapper"); + is(groupWrapper.instances[0].node, btn, "Button should be that instance."); + + CustomizableUI.removeWidgetFromArea(BUTTONID); + + otherSingleWrapper = groupWrapper.forWindow(window); + isnot(singleWrapper, otherSingleWrapper, "Shouldn't get the same wrapper after removing it from the navbar."); + singleWrapper = otherSingleWrapper; + is(singleWrapper.node, btn, "Node should be in the wrapper"); + is(groupWrapper.instances.length, 1, "There should be 1 instance on the group wrapper"); + is(groupWrapper.instances[0].node, btn, "Button should be that instance."); + + btn.remove(); + otherSingleWrapper = groupWrapper.forWindow(window); + is(singleWrapper, otherSingleWrapper, "Should get the same wrapper after physically removing the node."); + is(singleWrapper.node, null, "Wrapper's node should be null now that it's left the DOM."); + is(groupWrapper.instances.length, 1, "There should be 1 instance on the group wrapper"); + is(groupWrapper.instances[0].node, null, "That instance should be null."); + + btn = createDummyXULButton(BUTTONID, "XUL btn"); + gNavToolbox.palette.appendChild(btn); + otherSingleWrapper = groupWrapper.forWindow(window); + is(singleWrapper, otherSingleWrapper, "Should get the same wrapper after readding the node."); + is(singleWrapper.node, btn, "Node should be in the wrapper"); + is(groupWrapper.instances.length, 1, "There should be 1 instance on the group wrapper"); + is(groupWrapper.instances[0].node, btn, "Button should be that instance."); + + CustomizableUI.addWidgetToArea(BUTTONID, CustomizableUI.AREA_NAVBAR); + + otherSingleWrapper = groupWrapper.forWindow(window); + is(singleWrapper, otherSingleWrapper, "Should get the same wrapper after adding the node to the navbar."); + is(singleWrapper.node, btn, "Node should be in the wrapper"); + is(groupWrapper.instances.length, 1, "There should be 1 instance on the group wrapper"); + is(groupWrapper.instances[0].node, btn, "Button should be that instance."); + + CustomizableUI.removeWidgetFromArea(BUTTONID); + + otherSingleWrapper = groupWrapper.forWindow(window); + isnot(singleWrapper, otherSingleWrapper, "Shouldn't get the same wrapper after removing it from the navbar."); + singleWrapper = otherSingleWrapper; + is(singleWrapper.node, btn, "Node should be in the wrapper"); + is(groupWrapper.instances.length, 1, "There should be 1 instance on the group wrapper"); + is(groupWrapper.instances[0].node, btn, "Button should be that instance."); + + btn.remove(); + otherSingleWrapper = groupWrapper.forWindow(window); + is(singleWrapper, otherSingleWrapper, "Should get the same wrapper after physically removing the node."); + is(singleWrapper.node, null, "Wrapper's node should be null now that it's left the DOM."); + is(groupWrapper.instances.length, 1, "There should be 1 instance on the group wrapper"); + is(groupWrapper.instances[0].node, null, "That instance should be null."); +}); diff --git a/browser/components/customizableui/test/browser_987185_syncButton.js b/browser/components/customizableui/test/browser_987185_syncButton.js new file mode 100755 index 000000000..988d738be --- /dev/null +++ b/browser/components/customizableui/test/browser_987185_syncButton.js @@ -0,0 +1,77 @@ +/* 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"; + +var syncService = {}; +Components.utils.import("resource://services-sync/service.js", syncService); + +var needsSetup; +var originalSync; +var service = syncService.Service; +var syncWasCalled = false; + +add_task(function* testSyncButtonFunctionality() { + info("Check Sync button functionality"); + storeInitialValues(); + mockFunctions(); + + // add the Sync button to the panel + CustomizableUI.addWidgetToArea("sync-button", CustomizableUI.AREA_PANEL); + + // check the button's functionality + yield PanelUI.show(); + info("The panel menu was opened"); + + let syncButton = document.getElementById("sync-button"); + ok(syncButton, "The Sync button was added to the Panel Menu"); + // click the button - the panel should open. + syncButton.click(); + let syncPanel = document.getElementById("PanelUI-remotetabs"); + ok(syncPanel.getAttribute("current"), "Sync Panel is in view"); + + // Find and click the "setup" button. + let syncNowButton = document.getElementById("PanelUI-remotetabs-syncnow"); + syncNowButton.click(); + + info("The sync button was clicked"); + + yield waitForCondition(() => syncWasCalled); +}); + +add_task(function* asyncCleanup() { + // reset the panel UI to the default state + yield resetCustomization(); + ok(CustomizableUI.inDefaultState, "The panel UI is in default state again."); + + if (isPanelUIOpen()) { + let panelHidePromise = promisePanelHidden(window); + PanelUI.hide(); + yield panelHidePromise; + } + + restoreValues(); +}); + +function mockFunctions() { + // mock needsSetup + gSyncUI._needsSetup = () => Promise.resolve(false); + + // mock service.errorHandler.syncAndReportErrors() + service.errorHandler.syncAndReportErrors = mocked_syncAndReportErrors; +} + +function mocked_syncAndReportErrors() { + syncWasCalled = true; +} + +function restoreValues() { + gSyncUI._needsSetup = needsSetup; + service.sync = originalSync; +} + +function storeInitialValues() { + needsSetup = gSyncUI._needsSetup; + originalSync = service.sync; +} diff --git a/browser/components/customizableui/test/browser_987492_window_api.js b/browser/components/customizableui/test/browser_987492_window_api.js new file mode 100644 index 000000000..1718303e1 --- /dev/null +++ b/browser/components/customizableui/test/browser_987492_window_api.js @@ -0,0 +1,54 @@ +/* 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"; + + +add_task(function* testOneWindow() { + let windows = []; + for (let win of CustomizableUI.windows) + windows.push(win); + is(windows.length, 1, "Should have one customizable window"); +}); + + +add_task(function* testOpenCloseWindow() { + let newWindow = null; + let openListener = { + onWindowOpened: function(window) { + newWindow = window; + } + } + CustomizableUI.addListener(openListener); + let win = yield openAndLoadWindow(null, true); + isnot(newWindow, null, "Should have gotten onWindowOpen event"); + is(newWindow, win, "onWindowOpen event should have received expected window"); + CustomizableUI.removeListener(openListener); + + let windows = []; + for (let win of CustomizableUI.windows) + windows.push(win); + is(windows.length, 2, "Should have two customizable windows"); + isnot(windows.indexOf(window), -1, "Current window should be in window collection."); + isnot(windows.indexOf(newWindow), -1, "New window should be in window collection."); + + let closedWindow = null; + let closeListener = { + onWindowClosed: function(window) { + closedWindow = window; + } + } + CustomizableUI.addListener(closeListener); + yield promiseWindowClosed(newWindow); + isnot(closedWindow, null, "Should have gotten onWindowClosed event") + is(newWindow, closedWindow, "Closed window should match previously opened window"); + CustomizableUI.removeListener(closeListener); + + windows = []; + for (let win of CustomizableUI.windows) + windows.push(win); + is(windows.length, 1, "Should have one customizable window"); + isnot(windows.indexOf(window), -1, "Current window should be in window collection."); + is(windows.indexOf(closedWindow), -1, "Closed window should not be in window collection."); +}); diff --git a/browser/components/customizableui/test/browser_987640_charEncoding.js b/browser/components/customizableui/test/browser_987640_charEncoding.js new file mode 100644 index 000000000..dfe02f940 --- /dev/null +++ b/browser/components/customizableui/test/browser_987640_charEncoding.js @@ -0,0 +1,60 @@ +/* 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 TEST_PAGE = "http://mochi.test:8888/browser/browser/components/customizableui/test/support/test_967000_charEncoding_page.html"; + +add_task(function*() { + info("Check Character Encoding panel functionality"); + + // add the Character Encoding button to the panel + CustomizableUI.addWidgetToArea("characterencoding-button", + CustomizableUI.AREA_PANEL); + + let newTab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, TEST_PAGE, true, true); + + yield PanelUI.show(); + let charEncodingButton = document.getElementById("characterencoding-button"); + let characterEncodingView = document.getElementById("PanelUI-characterEncodingView"); + let subviewShownPromise = subviewShown(characterEncodingView); + charEncodingButton.click(); + yield subviewShownPromise; + + let checkedButtons = characterEncodingView.querySelectorAll("toolbarbutton[checked='true']"); + let initialEncoding = checkedButtons[0]; + is(initialEncoding.getAttribute("label"), "Unicode", "The unicode encoding is initially selected"); + + // change the encoding + let encodings = characterEncodingView.querySelectorAll("toolbarbutton"); + let newEncoding = encodings[0].hasAttribute("checked") ? encodings[1] : encodings[0]; + let tabLoadPromise = promiseTabLoadEvent(gBrowser.selectedTab, TEST_PAGE); + newEncoding.click(); + yield tabLoadPromise; + + // check that the new encodng is applied + yield PanelUI.show(); + charEncodingButton.click(); + checkedButtons = characterEncodingView.querySelectorAll("toolbarbutton[checked='true']"); + let selectedEncodingName = checkedButtons[0].getAttribute("label"); + ok(selectedEncodingName != "Unicode", "The encoding was changed to " + selectedEncodingName); + + // reset the initial encoding + yield PanelUI.show(); + charEncodingButton.click(); + tabLoadPromise = promiseTabLoadEvent(gBrowser.selectedTab, TEST_PAGE); + initialEncoding.click(); + yield tabLoadPromise; + yield PanelUI.show(); + charEncodingButton.click(); + checkedButtons = characterEncodingView.querySelectorAll("toolbarbutton[checked='true']"); + is(checkedButtons[0].getAttribute("label"), "Unicode", "The encoding was reset to Unicode"); + yield BrowserTestUtils.removeTab(newTab); +}); + +add_task(function* asyncCleanup() { + // reset the panel to the default state + yield resetCustomization(); + ok(CustomizableUI.inDefaultState, "The UI is in default state again."); +}); diff --git a/browser/components/customizableui/test/browser_988072_sidebar_events.js b/browser/components/customizableui/test/browser_988072_sidebar_events.js new file mode 100644 index 000000000..6791be67a --- /dev/null +++ b/browser/components/customizableui/test/browser_988072_sidebar_events.js @@ -0,0 +1,392 @@ +/* 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"; + +var gSidebarMenu = document.getElementById("viewSidebarMenu"); +var gTestSidebarItem = null; + +var EVENTS = { + click: 0, command: 0, + onclick: 0, oncommand: 0 +}; + +window.sawEvent = function(event, isattr) { + let type = (isattr ? "on" : "") + event.type + EVENTS[type]++; +}; + +registerCleanupFunction(() => { + delete window.sawEvent; + + // Ensure sidebar is hidden after each test: + if (!document.getElementById("sidebar-box").hidden) { + SidebarUI.hide(); + } +}); + +function checkExpectedEvents(expected) { + for (let type of Object.keys(EVENTS)) { + let count = (type in expected ? expected[type] : 0); + is(EVENTS[type], count, "Should have seen the right number of " + type + " events"); + EVENTS[type] = 0; + } +} + +function createSidebarItem() { + gTestSidebarItem = document.createElement("menuitem"); + gTestSidebarItem.id = "testsidebar"; + gTestSidebarItem.setAttribute("label", "Test Sidebar"); + gSidebarMenu.insertBefore(gTestSidebarItem, gSidebarMenu.firstChild); +} + +function addWidget() { + CustomizableUI.addWidgetToArea("sidebar-button", "nav-bar"); + PanelUI.disableSingleSubviewPanelAnimations(); +} + +function removeWidget() { + CustomizableUI.removeWidgetFromArea("sidebar-button"); + PanelUI.enableSingleSubviewPanelAnimations(); +} + +// Filters out the trailing menuseparators from the sidebar list +function getSidebarList() { + let sidebars = [...gSidebarMenu.children].filter(sidebar => { + if (sidebar.localName == "menuseparator") + return false; + if (sidebar.getAttribute("hidden") == "true") + return false; + return true; + }); + return sidebars; +} + +function compareElements(original, displayed) { + let attrs = ["label", "key", "disabled", "hidden", "origin", "image", "checked"]; + for (let attr of attrs) { + is(displayed.getAttribute(attr), original.getAttribute(attr), "Should have the same " + attr + " attribute"); + } +} + +function compareList(original, displayed) { + is(displayed.length, original.length, "Should have the same number of children"); + + for (let i = 0; i < Math.min(original.length, displayed.length); i++) { + compareElements(displayed[i], original[i]); + } +} + +var showSidebarPopup = Task.async(function*() { + let button = document.getElementById("sidebar-button"); + let subview = document.getElementById("PanelUI-sidebar"); + + let popupShownPromise = BrowserTestUtils.waitForEvent(document, "popupshown"); + + let subviewShownPromise = subviewShown(subview); + EventUtils.synthesizeMouseAtCenter(button, {}); + return Promise.all([subviewShownPromise, popupShownPromise]); +}); + +// Check the sidebar widget shows the default items +add_task(function*() { + addWidget(); + + yield showSidebarPopup(); + + let sidebars = getSidebarList(); + let displayed = [...document.getElementById("PanelUI-sidebarItems").children]; + compareList(sidebars, displayed); + + let subview = document.getElementById("PanelUI-sidebar"); + let subviewHiddenPromise = subviewHidden(subview); + document.getElementById("customizationui-widget-panel").hidePopup(); + yield subviewHiddenPromise; + + removeWidget(); +}); + +function add_sidebar_task(description, setup, teardown) { + add_task(function*() { + info(description); + createSidebarItem(); + addWidget(); + yield setup(); + + CustomizableUI.addWidgetToArea("sidebar-button", "nav-bar"); + + yield showSidebarPopup(); + + let sidebars = getSidebarList(); + let displayed = [...document.getElementById("PanelUI-sidebarItems").children]; + compareList(sidebars, displayed); + + is(displayed[0].label, "Test Sidebar", "Should have the right element at the top"); + let subview = document.getElementById("PanelUI-sidebar"); + let subviewHiddenPromise = subviewHidden(subview); + EventUtils.synthesizeMouseAtCenter(displayed[0], {}); + yield subviewHiddenPromise; + + yield teardown(); + gTestSidebarItem.remove(); + removeWidget(); + }); +} + +add_sidebar_task( + "Check that a sidebar that uses a command event listener works", +function*() { + gTestSidebarItem.addEventListener("command", window.sawEvent); +}, function*() { + checkExpectedEvents({ command: 1 }); +}); + +add_sidebar_task( + "Check that a sidebar that uses a click event listener works", +function*() { + gTestSidebarItem.addEventListener("click", window.sawEvent); +}, function*() { + checkExpectedEvents({ click: 1 }); +}); + +add_sidebar_task( + "Check that a sidebar that uses both click and command event listeners works", +function*() { + gTestSidebarItem.addEventListener("command", window.sawEvent); + gTestSidebarItem.addEventListener("click", window.sawEvent); +}, function*() { + checkExpectedEvents({ command: 1, click: 1 }); +}); + +add_sidebar_task( + "Check that a sidebar that uses an oncommand attribute works", +function*() { + gTestSidebarItem.setAttribute("oncommand", "window.sawEvent(event, true)"); +}, function*() { + checkExpectedEvents({ oncommand: 1 }); +}); + +add_sidebar_task( + "Check that a sidebar that uses an onclick attribute works", +function*() { + gTestSidebarItem.setAttribute("onclick", "window.sawEvent(event, true)"); +}, function*() { + checkExpectedEvents({ onclick: 1 }); +}); + +add_sidebar_task( + "Check that a sidebar that uses both onclick and oncommand attributes works", +function*() { + gTestSidebarItem.setAttribute("onclick", "window.sawEvent(event, true)"); + gTestSidebarItem.setAttribute("oncommand", "window.sawEvent(event, true)"); +}, function*() { + checkExpectedEvents({ onclick: 1, oncommand: 1 }); +}); + +add_sidebar_task( + "Check that a sidebar that uses an onclick attribute and a command listener works", +function*() { + gTestSidebarItem.setAttribute("onclick", "window.sawEvent(event, true)"); + gTestSidebarItem.addEventListener("command", window.sawEvent); +}, function*() { + checkExpectedEvents({ onclick: 1, command: 1 }); +}); + +add_sidebar_task( + "Check that a sidebar that uses an oncommand attribute and a click listener works", +function*() { + gTestSidebarItem.setAttribute("oncommand", "window.sawEvent(event, true)"); + gTestSidebarItem.addEventListener("click", window.sawEvent); +}, function*() { + checkExpectedEvents({ click: 1, oncommand: 1 }); +}); + +add_sidebar_task( + "A sidebar with both onclick attribute and click listener sees only one event :(", +function*() { + gTestSidebarItem.setAttribute("onclick", "window.sawEvent(event, true)"); + gTestSidebarItem.addEventListener("click", window.sawEvent); +}, function*() { + checkExpectedEvents({ onclick: 1 }); +}); + +add_sidebar_task( + "A sidebar with both oncommand attribute and command listener sees only one event :(", +function*() { + gTestSidebarItem.setAttribute("oncommand", "window.sawEvent(event, true)"); + gTestSidebarItem.addEventListener("command", window.sawEvent); +}, function*() { + checkExpectedEvents({ oncommand: 1 }); +}); + +add_sidebar_task( + "Check that a sidebar that uses a broadcaster with an oncommand attribute works", +function*() { + let broadcaster = document.createElement("broadcaster"); + broadcaster.setAttribute("id", "testbroadcaster"); + broadcaster.setAttribute("oncommand", "window.sawEvent(event, true)"); + broadcaster.setAttribute("label", "Test Sidebar"); + document.getElementById("mainBroadcasterSet").appendChild(broadcaster); + + gTestSidebarItem.setAttribute("observes", "testbroadcaster"); +}, function*() { + checkExpectedEvents({ oncommand: 1 }); + document.getElementById("testbroadcaster").remove(); +}); + +add_sidebar_task( + "Check that a sidebar that uses a broadcaster with an onclick attribute works", +function*() { + let broadcaster = document.createElement("broadcaster"); + broadcaster.setAttribute("id", "testbroadcaster"); + broadcaster.setAttribute("onclick", "window.sawEvent(event, true)"); + broadcaster.setAttribute("label", "Test Sidebar"); + document.getElementById("mainBroadcasterSet").appendChild(broadcaster); + + gTestSidebarItem.setAttribute("observes", "testbroadcaster"); +}, function*() { + checkExpectedEvents({ onclick: 1 }); + document.getElementById("testbroadcaster").remove(); +}); + +add_sidebar_task( + "Check that a sidebar that uses a broadcaster with both onclick and oncommand attributes works", +function*() { + let broadcaster = document.createElement("broadcaster"); + broadcaster.setAttribute("id", "testbroadcaster"); + broadcaster.setAttribute("onclick", "window.sawEvent(event, true)"); + broadcaster.setAttribute("oncommand", "window.sawEvent(event, true)"); + broadcaster.setAttribute("label", "Test Sidebar"); + document.getElementById("mainBroadcasterSet").appendChild(broadcaster); + + gTestSidebarItem.setAttribute("observes", "testbroadcaster"); +}, function*() { + checkExpectedEvents({ onclick: 1, oncommand: 1 }); + document.getElementById("testbroadcaster").remove(); +}); + +add_sidebar_task( + "Check that a sidebar with a click listener and a broadcaster with an oncommand attribute works", +function*() { + let broadcaster = document.createElement("broadcaster"); + broadcaster.setAttribute("id", "testbroadcaster"); + broadcaster.setAttribute("oncommand", "window.sawEvent(event, true)"); + broadcaster.setAttribute("label", "Test Sidebar"); + document.getElementById("mainBroadcasterSet").appendChild(broadcaster); + + gTestSidebarItem.setAttribute("observes", "testbroadcaster"); + gTestSidebarItem.addEventListener("click", window.sawEvent); +}, function*() { + checkExpectedEvents({ click: 1, oncommand: 1 }); + document.getElementById("testbroadcaster").remove(); +}); + +add_sidebar_task( + "Check that a sidebar with a command listener and a broadcaster with an onclick attribute works", +function*() { + let broadcaster = document.createElement("broadcaster"); + broadcaster.setAttribute("id", "testbroadcaster"); + broadcaster.setAttribute("onclick", "window.sawEvent(event, true)"); + broadcaster.setAttribute("label", "Test Sidebar"); + document.getElementById("mainBroadcasterSet").appendChild(broadcaster); + + gTestSidebarItem.setAttribute("observes", "testbroadcaster"); + gTestSidebarItem.addEventListener("command", window.sawEvent); +}, function*() { + checkExpectedEvents({ onclick: 1, command: 1 }); + document.getElementById("testbroadcaster").remove(); +}); + +add_sidebar_task( + "Check that a sidebar with a click listener and a broadcaster with an onclick " + + "attribute only sees one event :(", +function*() { + let broadcaster = document.createElement("broadcaster"); + broadcaster.setAttribute("id", "testbroadcaster"); + broadcaster.setAttribute("onclick", "window.sawEvent(event, true)"); + broadcaster.setAttribute("label", "Test Sidebar"); + document.getElementById("mainBroadcasterSet").appendChild(broadcaster); + + gTestSidebarItem.setAttribute("observes", "testbroadcaster"); + gTestSidebarItem.addEventListener("click", window.sawEvent); +}, function*() { + checkExpectedEvents({ onclick: 1 }); + document.getElementById("testbroadcaster").remove(); +}); + +add_sidebar_task( + "Check that a sidebar with a command listener and a broadcaster with an oncommand " + + "attribute only sees one event :(", +function*() { + let broadcaster = document.createElement("broadcaster"); + broadcaster.setAttribute("id", "testbroadcaster"); + broadcaster.setAttribute("oncommand", "window.sawEvent(event, true)"); + broadcaster.setAttribute("label", "Test Sidebar"); + document.getElementById("mainBroadcasterSet").appendChild(broadcaster); + + gTestSidebarItem.setAttribute("observes", "testbroadcaster"); + gTestSidebarItem.addEventListener("command", window.sawEvent); +}, function*() { + checkExpectedEvents({ oncommand: 1 }); + document.getElementById("testbroadcaster").remove(); +}); + +add_sidebar_task( + "Check that a sidebar that uses a command element with a command event listener works", +function*() { + let command = document.createElement("command"); + command.setAttribute("id", "testcommand"); + document.getElementById("mainCommandSet").appendChild(command); + command.addEventListener("command", window.sawEvent); + + gTestSidebarItem.setAttribute("command", "testcommand"); +}, function*() { + checkExpectedEvents({ command: 1 }); + document.getElementById("testcommand").remove(); +}); + +add_sidebar_task( + "Check that a sidebar that uses a command element with an oncommand attribute works", +function*() { + let command = document.createElement("command"); + command.setAttribute("id", "testcommand"); + command.setAttribute("oncommand", "window.sawEvent(event, true)"); + document.getElementById("mainCommandSet").appendChild(command); + + gTestSidebarItem.setAttribute("command", "testcommand"); +}, function*() { + checkExpectedEvents({ oncommand: 1 }); + document.getElementById("testcommand").remove(); +}); + +add_sidebar_task("Check that a sidebar that uses a command element with a " + + "command event listener and oncommand attribute works", +function*() { + let command = document.createElement("command"); + command.setAttribute("id", "testcommand"); + command.setAttribute("oncommand", "window.sawEvent(event, true)"); + document.getElementById("mainCommandSet").appendChild(command); + command.addEventListener("command", window.sawEvent); + + gTestSidebarItem.setAttribute("command", "testcommand"); +}, function*() { + checkExpectedEvents({ command: 1, oncommand: 1 }); + document.getElementById("testcommand").remove(); +}); + +add_sidebar_task( + "A sidebar with a command element will still see click events", +function*() { + let command = document.createElement("command"); + command.setAttribute("id", "testcommand"); + command.setAttribute("oncommand", "window.sawEvent(event, true)"); + document.getElementById("mainCommandSet").appendChild(command); + command.addEventListener("command", window.sawEvent); + + gTestSidebarItem.setAttribute("command", "testcommand"); + gTestSidebarItem.addEventListener("click", window.sawEvent); +}, function*() { + checkExpectedEvents({ click: 1, command: 1, oncommand: 1 }); + document.getElementById("testcommand").remove(); +}); diff --git a/browser/components/customizableui/test/browser_989338_saved_placements_not_resaved.js b/browser/components/customizableui/test/browser_989338_saved_placements_not_resaved.js new file mode 100644 index 000000000..2a1b01bf7 --- /dev/null +++ b/browser/components/customizableui/test/browser_989338_saved_placements_not_resaved.js @@ -0,0 +1,56 @@ +/* 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 BUTTONID = "test-widget-saved-earlier"; +const AREAID = "test-area-saved-earlier"; + +var hadSavedState; +function test() { + // Hack our way into the module to fake a saved state that isn't there... + let backstagePass = Cu.import("resource:///modules/CustomizableUI.jsm", {}); + hadSavedState = backstagePass.gSavedState != null; + if (!hadSavedState) { + backstagePass.gSavedState = {placements: {}}; + } + backstagePass.gSavedState.placements[AREAID] = [BUTTONID]; + // Put bogus stuff in the saved state for the nav-bar, so as to check the current placements + // override this one... + backstagePass.gSavedState.placements[CustomizableUI.AREA_NAVBAR] = ["bogus-navbar-item"]; + + backstagePass.gDirty = true; + backstagePass.CustomizableUIInternal.saveState(); + + let newSavedState = JSON.parse(Services.prefs.getCharPref("browser.uiCustomization.state")); + let savedArea = Array.isArray(newSavedState.placements[AREAID]); + ok(savedArea, "Should have re-saved the state, even though the area isn't registered"); + + if (savedArea) { + placementArraysEqual(AREAID, newSavedState.placements[AREAID], [BUTTONID]); + } + ok(!backstagePass.gPlacements.has(AREAID), "Placements map shouldn't have been affected"); + + let savedNavbar = Array.isArray(newSavedState.placements[CustomizableUI.AREA_NAVBAR]); + ok(savedNavbar, "Should have saved nav-bar contents"); + if (savedNavbar) { + placementArraysEqual(CustomizableUI.AREA_NAVBAR, newSavedState.placements[CustomizableUI.AREA_NAVBAR], + CustomizableUI.getWidgetIdsInArea(CustomizableUI.AREA_NAVBAR)); + } +} + +registerCleanupFunction(function() { + let backstagePass = Cu.import("resource:///modules/CustomizableUI.jsm", {}); + if (!hadSavedState) { + backstagePass.gSavedState = null; + } else { + let savedPlacements = backstagePass.gSavedState.placements; + delete savedPlacements[AREAID]; + let realNavBarPlacements = CustomizableUI.getWidgetIdsInArea(CustomizableUI.AREA_NAVBAR); + savedPlacements[CustomizableUI.AREA_NAVBAR] = realNavBarPlacements; + } + backstagePass.gDirty = true; + backstagePass.CustomizableUIInternal.saveState(); +}); + diff --git a/browser/components/customizableui/test/browser_989751_subviewbutton_class.js b/browser/components/customizableui/test/browser_989751_subviewbutton_class.js new file mode 100644 index 000000000..0d11324ed --- /dev/null +++ b/browser/components/customizableui/test/browser_989751_subviewbutton_class.js @@ -0,0 +1,62 @@ +/* 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 kCustomClass = "acustomclassnoonewilluse"; +var tempElement = null; + +function insertClassNameToMenuChildren(parentMenu) { + let el = parentMenu.querySelector("menuitem:first-of-type"); + el.classList.add(kCustomClass); + tempElement = el; +} + +function checkSubviewButtonClass(menuId, buttonId, subviewId) { + return function*() { + info("Checking for items without the subviewbutton class in " + buttonId + " widget"); + let menu = document.getElementById(menuId); + insertClassNameToMenuChildren(menu); + + let placement = CustomizableUI.getPlacementOfWidget(buttonId); + let changedPlacement = false; + if (!placement || placement.area != CustomizableUI.AREA_PANEL) { + CustomizableUI.addWidgetToArea(buttonId, CustomizableUI.AREA_PANEL); + changedPlacement = true; + } + yield PanelUI.show(); + + let button = document.getElementById(buttonId); + button.click(); + + yield waitForCondition(() => !PanelUI.multiView.hasAttribute("transitioning")); + let subview = document.getElementById(subviewId); + ok(subview.firstChild, "Subview should have a kid"); + let subviewchildren = subview.querySelectorAll("toolbarbutton"); + for (let i = 0; i < subviewchildren.length; i++) { + let item = subviewchildren[i]; + let itemReadable = "Item '" + item.label + "' (classes: " + item.className + ")"; + ok(item.classList.contains("subviewbutton"), itemReadable + " should have the subviewbutton class."); + if (i == 0) { + ok(item.classList.contains(kCustomClass), itemReadable + " should still have its own class, too."); + } + } + + let panelHiddenPromise = promisePanelHidden(window); + PanelUI.hide(); + yield panelHiddenPromise; + + if (changedPlacement) { + CustomizableUI.reset(); + } + }; +} + +add_task(checkSubviewButtonClass("menuWebDeveloperPopup", "developer-button", "PanelUI-developerItems")); +add_task(checkSubviewButtonClass("viewSidebarMenu", "sidebar-button", "PanelUI-sidebarItems")); + +registerCleanupFunction(function() { + tempElement.classList.remove(kCustomClass) + tempElement = null; +}); diff --git a/browser/components/customizableui/test/browser_992747_toggle_noncustomizable_toolbar.js b/browser/components/customizableui/test/browser_992747_toggle_noncustomizable_toolbar.js new file mode 100644 index 000000000..eb0a8c8ee --- /dev/null +++ b/browser/components/customizableui/test/browser_992747_toggle_noncustomizable_toolbar.js @@ -0,0 +1,26 @@ +/* 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 TOOLBARID = "test-noncustomizable-toolbar-for-toggling"; +function test() { + let tb = document.createElementNS(kNSXUL, "toolbar"); + tb.id = TOOLBARID; + gNavToolbox.appendChild(tb); + try { + CustomizableUI.setToolbarVisibility(TOOLBARID, false); + } catch (ex) { + ok(false, "Should not throw exceptions trying to set toolbar visibility."); + } + is(tb.getAttribute("collapsed"), "true", "Toolbar should be collapsed"); + try { + CustomizableUI.setToolbarVisibility(TOOLBARID, true); + } catch (ex) { + ok(false, "Should not throw exceptions trying to set toolbar visibility."); + } + is(tb.getAttribute("collapsed"), "false", "Toolbar should be uncollapsed"); + tb.remove(); +} + diff --git a/browser/components/customizableui/test/browser_993322_widget_notoolbar.js b/browser/components/customizableui/test/browser_993322_widget_notoolbar.js new file mode 100644 index 000000000..9264eb78a --- /dev/null +++ b/browser/components/customizableui/test/browser_993322_widget_notoolbar.js @@ -0,0 +1,36 @@ +/* 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 BUTTONID = "test-API-created-widget-toolbar-gone"; +const TOOLBARID = "test-API-created-extra-toolbar"; + +add_task(function*() { + let toolbar = createToolbarWithPlacements(TOOLBARID, []); + CustomizableUI.addWidgetToArea(BUTTONID, TOOLBARID); + is(CustomizableUI.getPlacementOfWidget(BUTTONID).area, TOOLBARID, "Should be on toolbar"); + is(toolbar.children.length, 0, "Toolbar has no kid"); + + CustomizableUI.unregisterArea(TOOLBARID); + CustomizableUI.createWidget({id: BUTTONID, label: "Test widget toolbar gone"}); + + let currentWidget = CustomizableUI.getWidget(BUTTONID); + + yield startCustomizing(); + let buttonNode = document.getElementById(BUTTONID); + ok(buttonNode, "Should find button in window"); + if (buttonNode) { + is(buttonNode.parentNode.localName, "toolbarpaletteitem", "Node should be wrapped"); + is(buttonNode.parentNode.getAttribute("place"), "palette", "Node should be in palette"); + is(buttonNode, gNavToolbox.palette.querySelector("#" + BUTTONID), "Node should really be in palette."); + } + is(currentWidget.forWindow(window).node, buttonNode, "Should have the same node for customize mode"); + yield endCustomizing(); + + CustomizableUI.destroyWidget(BUTTONID); + CustomizableUI.unregisterArea(TOOLBARID, true); + toolbar.remove(); + gAddedToolbars.clear(); +}); diff --git a/browser/components/customizableui/test/browser_995164_registerArea_during_customize_mode.js b/browser/components/customizableui/test/browser_995164_registerArea_during_customize_mode.js new file mode 100644 index 000000000..4d292a929 --- /dev/null +++ b/browser/components/customizableui/test/browser_995164_registerArea_during_customize_mode.js @@ -0,0 +1,149 @@ +/* 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 TOOLBARID = "test-toolbar-added-during-customize-mode"; + +// The ID of a button that is not placed (ie, is in the palette) by default +const kNonPlacedWidgetId = "open-file-button"; + +add_task(function*() { + yield startCustomizing(); + let toolbar = createToolbarWithPlacements(TOOLBARID, []); + CustomizableUI.addWidgetToArea(kNonPlacedWidgetId, TOOLBARID); + let button = document.getElementById(kNonPlacedWidgetId); + ok(button, "Button should exist."); + is(button.parentNode.localName, "toolbarpaletteitem", "Button's parent node should be a wrapper."); + + simulateItemDrag(button, gNavToolbox.palette); + ok(!CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId), "Button moved to the palette"); + ok(gNavToolbox.palette.querySelector(`#${kNonPlacedWidgetId}`), "Button really is in palette."); + + button.scrollIntoView(); + simulateItemDrag(button, toolbar); + ok(CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId), "Button moved out of palette"); + is(CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId).area, TOOLBARID, "Button's back on toolbar"); + ok(toolbar.querySelector(`#${kNonPlacedWidgetId}`), "Button really is on toolbar."); + + yield endCustomizing(); + isnot(button.parentNode.localName, "toolbarpaletteitem", "Button's parent node should not be a wrapper outside customize mode."); + yield startCustomizing(); + + is(button.parentNode.localName, "toolbarpaletteitem", "Button's parent node should be a wrapper back in customize mode."); + + simulateItemDrag(button, gNavToolbox.palette); + ok(!CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId), "Button moved to the palette"); + ok(gNavToolbox.palette.querySelector(`#${kNonPlacedWidgetId}`), "Button really is in palette."); + + ok(!CustomizableUI.inDefaultState, "Not in default state while toolbar is not collapsed yet."); + setToolbarVisibility(toolbar, false); + ok(CustomizableUI.inDefaultState, "In default state while toolbar is collapsed."); + + setToolbarVisibility(toolbar, true); + + info("Check that removing the area registration from within customize mode works"); + CustomizableUI.unregisterArea(TOOLBARID); + ok(CustomizableUI.inDefaultState, "Now that the toolbar is no longer registered, should be in default state."); + ok(!gCustomizeMode.areas.has(toolbar), "Toolbar shouldn't be known to customize mode."); + + CustomizableUI.registerArea(TOOLBARID, {legacy: true, defaultPlacements: []}); + CustomizableUI.registerToolbarNode(toolbar, []); + ok(!CustomizableUI.inDefaultState, "Now that the toolbar is registered again, should no longer be in default state."); + ok(gCustomizeMode.areas.has(toolbar), "Toolbar should be known to customize mode again."); + + button.scrollIntoView(); + simulateItemDrag(button, toolbar); + ok(CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId), "Button moved out of palette"); + is(CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId).area, TOOLBARID, "Button's back on toolbar"); + ok(toolbar.querySelector(`#${kNonPlacedWidgetId}`), "Button really is on toolbar."); + + let otherWin = yield openAndLoadWindow({}, true); + let otherTB = otherWin.document.createElementNS(kNSXUL, "toolbar"); + otherTB.id = TOOLBARID; + otherTB.setAttribute("customizable", "true"); + let wasInformedCorrectlyOfAreaAppearing = false; + let listener = { + onAreaNodeRegistered: function(aArea, aNode) { + if (aNode == otherTB) { + wasInformedCorrectlyOfAreaAppearing = true; + } + } + }; + CustomizableUI.addListener(listener); + otherWin.gNavToolbox.appendChild(otherTB); + ok(wasInformedCorrectlyOfAreaAppearing, "Should have been told area was registered."); + CustomizableUI.removeListener(listener); + + ok(otherTB.querySelector(`#${kNonPlacedWidgetId}`), "Button is on other toolbar, too."); + + simulateItemDrag(button, gNavToolbox.palette); + ok(!CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId), "Button moved to the palette"); + ok(gNavToolbox.palette.querySelector(`#${kNonPlacedWidgetId}`), "Button really is in palette."); + ok(!otherTB.querySelector(`#${kNonPlacedWidgetId}`), "Button is in palette in other window, too."); + + button.scrollIntoView(); + simulateItemDrag(button, toolbar); + ok(CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId), "Button moved out of palette"); + is(CustomizableUI.getPlacementOfWidget(kNonPlacedWidgetId).area, TOOLBARID, "Button's back on toolbar"); + ok(toolbar.querySelector(`#${kNonPlacedWidgetId}`), "Button really is on toolbar."); + ok(otherTB.querySelector(`#${kNonPlacedWidgetId}`), "Button is on other toolbar, too."); + + let wasInformedCorrectlyOfAreaDisappearing = false; + // XXXgijs So we could be using promiseWindowClosed here. However, after + // repeated random oranges, I'm instead relying on onWindowClosed below to + // fire appropriately - it is linked to an unload event as well, and so + // reusing it prevents a potential race between unload handlers where the + // one from promiseWindowClosed could fire before the onWindowClosed + // (and therefore onAreaNodeRegistered) one, causing the test to fail. + let windowCloseDeferred = Promise.defer(); + listener = { + onAreaNodeUnregistered: function(aArea, aNode, aReason) { + if (aArea == TOOLBARID) { + is(aNode, otherTB, "Should be informed about other toolbar"); + is(aReason, CustomizableUI.REASON_WINDOW_CLOSED, "Reason should be correct."); + wasInformedCorrectlyOfAreaDisappearing = (aReason === CustomizableUI.REASON_WINDOW_CLOSED); + } + }, + onWindowClosed: function(aWindow) { + if (aWindow == otherWin) { + windowCloseDeferred.resolve(aWindow); + } else { + info("Other window was closed!"); + info("Other window title: " + (aWindow.document && aWindow.document.title)); + info("Our window title: " + (otherWin.document && otherWin.document.title)); + } + }, + }; + CustomizableUI.addListener(listener); + otherWin.close(); + let windowClosed = yield windowCloseDeferred.promise; + + is(windowClosed, otherWin, "Window should have sent onWindowClosed notification."); + ok(wasInformedCorrectlyOfAreaDisappearing, "Should be told about window closing."); + // Closing the other window should not be counted against this window's customize mode: + is(button.parentNode.localName, "toolbarpaletteitem", "Button's parent node should still be a wrapper."); + ok(gCustomizeMode.areas.has(toolbar), "Toolbar should still be a customizable area for this customize mode instance."); + + yield gCustomizeMode.reset(); + + yield endCustomizing(); + + CustomizableUI.removeListener(listener); + wasInformedCorrectlyOfAreaDisappearing = false; + listener = { + onAreaNodeUnregistered: function(aArea, aNode, aReason) { + if (aArea == TOOLBARID) { + is(aNode, toolbar, "Should be informed about this window's toolbar"); + is(aReason, CustomizableUI.REASON_AREA_UNREGISTERED, "Reason for final removal should be correct."); + wasInformedCorrectlyOfAreaDisappearing = (aReason === CustomizableUI.REASON_AREA_UNREGISTERED); + } + }, + } + CustomizableUI.addListener(listener); + removeCustomToolbars(); + ok(wasInformedCorrectlyOfAreaDisappearing, "Should be told about area being unregistered."); + CustomizableUI.removeListener(listener); + ok(CustomizableUI.inDefaultState, "Should be fine after exiting customize mode."); +}); diff --git a/browser/components/customizableui/test/browser_996364_registerArea_different_properties.js b/browser/components/customizableui/test/browser_996364_registerArea_different_properties.js new file mode 100644 index 000000000..b9de5f687 --- /dev/null +++ b/browser/components/customizableui/test/browser_996364_registerArea_different_properties.js @@ -0,0 +1,112 @@ +/* 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"; + +// Calling CustomizableUI.registerArea twice with no +// properties should not throw an exception. +add_task(function() { + try { + CustomizableUI.registerArea("area-996364", {}); + CustomizableUI.registerArea("area-996364", {}); + } catch (ex) { + ok(false, ex.message); + } + + CustomizableUI.unregisterArea("area-996364", true); +}); + +add_task(function() { + let exceptionThrown = false; + try { + CustomizableUI.registerArea("area-996364-2", {type: CustomizableUI.TYPE_TOOLBAR, defaultCollapsed: "false"}); + } catch (ex) { + exceptionThrown = true; + } + ok(exceptionThrown, "defaultCollapsed is not allowed as an external property"); + + // No need to unregister the area because registration fails. +}); + +add_task(function() { + let exceptionThrown; + try { + CustomizableUI.registerArea("area-996364-3", {type: CustomizableUI.TYPE_TOOLBAR}); + CustomizableUI.registerArea("area-996364-3", {type: CustomizableUI.TYPE_MENU_PANEL}); + } catch (ex) { + exceptionThrown = ex; + } + ok(exceptionThrown, "Exception expected, an area cannot change types: " + (exceptionThrown ? exceptionThrown : "[no exception]")); + + CustomizableUI.unregisterArea("area-996364-3", true); +}); + +add_task(function() { + let exceptionThrown; + try { + CustomizableUI.registerArea("area-996364-4", {type: CustomizableUI.TYPE_MENU_PANEL}); + CustomizableUI.registerArea("area-996364-4", {type: CustomizableUI.TYPE_TOOLBAR}); + } catch (ex) { + exceptionThrown = ex; + } + ok(exceptionThrown, "Exception expected, an area cannot change types: " + (exceptionThrown ? exceptionThrown : "[no exception]")); + + CustomizableUI.unregisterArea("area-996364-4", true); +}); + +add_task(function() { + let exceptionThrown; + try { + CustomizableUI.registerArea("area-996899-1", { anchor: "PanelUI-menu-button", + type: CustomizableUI.TYPE_MENU_PANEL, + defaultPlacements: [] }); + CustomizableUI.registerArea("area-996899-1", { anchor: "home-button", + type: CustomizableUI.TYPE_MENU_PANEL, + defaultPlacements: [] }); + } catch (ex) { + exceptionThrown = ex; + } + ok(!exceptionThrown, "Changing anchors shouldn't throw an exception: " + (exceptionThrown ? exceptionThrown : "[no exception]")); + CustomizableUI.unregisterArea("area-996899-1", true); +}); + +add_task(function() { + let exceptionThrown; + try { + CustomizableUI.registerArea("area-996899-2", { anchor: "PanelUI-menu-button", + type: CustomizableUI.TYPE_MENU_PANEL, + defaultPlacements: [] }); + CustomizableUI.registerArea("area-996899-2", { anchor: "PanelUI-menu-button", + type: CustomizableUI.TYPE_MENU_PANEL, + defaultPlacements: ["feed-button"] }); + } catch (ex) { + exceptionThrown = ex; + } + ok(!exceptionThrown, "Changing defaultPlacements shouldn't throw an exception: " + (exceptionThrown ? exceptionThrown : "[no exception]")); + CustomizableUI.unregisterArea("area-996899-2", true); +}); + +add_task(function() { + let exceptionThrown; + try { + CustomizableUI.registerArea("area-996899-3", { legacy: true }); + CustomizableUI.registerArea("area-996899-3", { legacy: false }); + } catch (ex) { + exceptionThrown = ex; + } + ok(exceptionThrown, "Changing 'legacy' should throw an exception: " + (exceptionThrown ? exceptionThrown : "[no exception]")); + CustomizableUI.unregisterArea("area-996899-3", true); +}); + +add_task(function() { + let exceptionThrown; + try { + CustomizableUI.registerArea("area-996899-4", { overflowable: true }); + CustomizableUI.registerArea("area-996899-4", { overflowable: false }); + } catch (ex) { + exceptionThrown = ex; + } + ok(exceptionThrown, "Changing 'overflowable' should throw an exception: " + (exceptionThrown ? exceptionThrown : "[no exception]")); + CustomizableUI.unregisterArea("area-996899-4", true); +}); diff --git a/browser/components/customizableui/test/browser_996635_remove_non_widgets.js b/browser/components/customizableui/test/browser_996635_remove_non_widgets.js new file mode 100644 index 000000000..14a446eec --- /dev/null +++ b/browser/components/customizableui/test/browser_996635_remove_non_widgets.js @@ -0,0 +1,43 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +"use strict"; + +// NB: This is testing what happens if something that /isn't/ a customizable +// widget gets used in CustomizableUI APIs. Don't use this as an example of +// what should happen in a "normal" case or how you should use the API. +function test() { + // First create a button that isn't customizable, and add it in the nav-bar, + // but not in the customizable part of it (the customization target) but + // next to the main (hamburger) menu button. + const buttonID = "Test-non-widget-non-removable-button"; + let btn = document.createElement("toolbarbutton"); + btn.id = buttonID; + btn.label = "Hi"; + btn.setAttribute("style", "width: 20px; height: 20px; background-color: red"); + document.getElementById("nav-bar").appendChild(btn); + registerCleanupFunction(function() { + btn.remove(); + }); + + // Now try to add this non-customizable button to the tabstrip. This will + // update the internal bookkeeping (ie placements) information, but shouldn't + // move the node. + CustomizableUI.addWidgetToArea(buttonID, CustomizableUI.AREA_TABSTRIP); + let placement = CustomizableUI.getPlacementOfWidget(buttonID); + // Check our bookkeeping + ok(placement, "Button should be placed"); + is(placement && placement.area, CustomizableUI.AREA_TABSTRIP, "Should be placed on tabstrip."); + // Check we didn't move the node. + is(btn.parentNode && btn.parentNode.id, "nav-bar", "Actual button should still be on navbar."); + + // Now remove the node again. This should remove the bookkeeping, but again + // not affect the actual node. + CustomizableUI.removeWidgetFromArea(buttonID); + placement = CustomizableUI.getPlacementOfWidget(buttonID); + // Check our bookkeeping: + ok(!placement, "Button should no longer have a placement."); + // Check our node. + is(btn.parentNode && btn.parentNode.id, "nav-bar", "Actual button should still be on navbar."); +} + diff --git a/browser/components/customizableui/test/browser_bootstrapped_custom_toolbar.js b/browser/components/customizableui/test/browser_bootstrapped_custom_toolbar.js new file mode 100644 index 000000000..2c5f0c79c --- /dev/null +++ b/browser/components/customizableui/test/browser_bootstrapped_custom_toolbar.js @@ -0,0 +1,81 @@ +/* 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"; + +requestLongerTimeout(2); + +const kTestBarID = "testBar"; +const kWidgetID = "characterencoding-button"; + +function createTestBar(aLegacy) { + let testBar = document.createElement("toolbar"); + testBar.id = kTestBarID; + testBar.setAttribute("customizable", "true"); + CustomizableUI.registerArea(kTestBarID, { + type: CustomizableUI.TYPE_TOOLBAR, + legacy: aLegacy, + }); + gNavToolbox.appendChild(testBar); + return testBar; +} + +/** + * Helper function that does the following: + * + * 1) Creates a custom toolbar and registers it + * with CustomizableUI. Sets the legacy attribute + * of the object passed to registerArea to aLegacy. + * 2) Adds the widget with ID aWidgetID to that new + * toolbar. + * 3) Enters customize mode and makes sure that the + * widget is still in the right toolbar. + * 4) Exits customize mode, then removes and deregisters + * the custom toolbar. + * 5) Checks that the widget has no placement. + * 6) Re-adds and re-registers a custom toolbar with the same + * ID and options as the first one. + * 7) Enters customize mode and checks that the widget is + * properly back in the toolbar. + * 8) Exits customize mode, removes and de-registers the + * toolbar, and resets the toolbars to default. + */ +function checkRestoredPresence(aWidgetID, aLegacy) { + return Task.spawn(function* () { + let testBar = createTestBar(aLegacy); + CustomizableUI.addWidgetToArea(aWidgetID, kTestBarID); + let placement = CustomizableUI.getPlacementOfWidget(aWidgetID); + is(placement.area, kTestBarID, + "Expected " + aWidgetID + " to be in the test toolbar"); + + CustomizableUI.unregisterArea(testBar.id); + testBar.remove(); + + placement = CustomizableUI.getPlacementOfWidget(aWidgetID); + is(placement, null, "Expected " + aWidgetID + " to be in the palette"); + + testBar = createTestBar(aLegacy); + + yield startCustomizing(); + placement = CustomizableUI.getPlacementOfWidget(aWidgetID); + is(placement.area, kTestBarID, + "Expected " + aWidgetID + " to be in the test toolbar"); + yield endCustomizing(); + + CustomizableUI.unregisterArea(testBar.id); + testBar.remove(); + + yield resetCustomization(); + }); +} + +add_task(function* () { + yield checkRestoredPresence("downloads-button", false); + yield checkRestoredPresence("downloads-button", true); +}); + +add_task(function* () { + yield checkRestoredPresence("characterencoding-button", false); + yield checkRestoredPresence("characterencoding-button", true); +}); diff --git a/browser/components/customizableui/test/browser_check_tooltips_in_navbar.js b/browser/components/customizableui/test/browser_check_tooltips_in_navbar.js new file mode 100644 index 000000000..31dd42ad8 --- /dev/null +++ b/browser/components/customizableui/test/browser_check_tooltips_in_navbar.js @@ -0,0 +1,14 @@ +/* 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"; + +add_task(function* check_tooltips_in_navbar() { + yield startCustomizing(); + let homeButtonWrapper = document.getElementById("wrapper-home-button"); + let homeButton = document.getElementById("home-button"); + is(homeButtonWrapper.getAttribute("tooltiptext"), homeButton.getAttribute("label"), "the wrapper's tooltip should match the button's label"); + ok(homeButtonWrapper.getAttribute("tooltiptext"), "the button should have tooltip text"); + yield endCustomizing(); +}); diff --git a/browser/components/customizableui/test/browser_customizemode_contextmenu_menubuttonstate.js b/browser/components/customizableui/test/browser_customizemode_contextmenu_menubuttonstate.js new file mode 100644 index 000000000..8e1950291 --- /dev/null +++ b/browser/components/customizableui/test/browser_customizemode_contextmenu_menubuttonstate.js @@ -0,0 +1,24 @@ +"use strict"; + +add_task(function*() { + ok(!PanelUI.menuButton.hasAttribute("open"), "Menu button should not be 'pressed' outside customize mode"); + yield startCustomizing(); + + is(PanelUI.menuButton.getAttribute("open"), "true", "Menu button should be 'pressed' when in customize mode"); + + let contextMenu = document.getElementById("customizationPanelItemContextMenu"); + let shownPromise = popupShown(contextMenu); + let newWindowButton = document.getElementById("wrapper-new-window-button"); + EventUtils.synthesizeMouse(newWindowButton, 2, 2, {type: "contextmenu", button: 2}); + yield shownPromise; + is(PanelUI.menuButton.getAttribute("open"), "true", "Menu button should be 'pressed' when in customize mode after opening a context menu"); + + let hiddenContextPromise = popupHidden(contextMenu); + contextMenu.hidePopup(); + yield hiddenContextPromise; + is(PanelUI.menuButton.getAttribute("open"), "true", "Menu button should be 'pressed' when in customize mode after hiding a context menu"); + yield endCustomizing(); + + ok(!PanelUI.menuButton.hasAttribute("open"), "Menu button should not be 'pressed' after ending customize mode"); +}); + diff --git a/browser/components/customizableui/test/browser_panel_toggle.js b/browser/components/customizableui/test/browser_panel_toggle.js new file mode 100644 index 000000000..4c286fb85 --- /dev/null +++ b/browser/components/customizableui/test/browser_panel_toggle.js @@ -0,0 +1,43 @@ +/* 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"; + +/** + * Test opening and closing the menu panel UI. + */ + +// Show and hide the menu panel programmatically without an event (like UITour.jsm would) +add_task(function*() { + let shownPromise = promisePanelShown(window); + PanelUI.show(); + yield shownPromise; + + is(PanelUI.panel.getAttribute("panelopen"), "true", "Check that panel has panelopen attribute"); + is(PanelUI.panel.state, "open", "Check that panel state is 'open'"); + + let hiddenPromise = promisePanelHidden(window); + PanelUI.hide(); + yield hiddenPromise; + + ok(!PanelUI.panel.hasAttribute("panelopen"), "Check that panel doesn't have the panelopen attribute"); + is(PanelUI.panel.state, "closed", "Check that panel state is 'closed'"); +}); + +// Toggle the menu panel open and closed +add_task(function*() { + let shownPromise = promisePanelShown(window); + PanelUI.toggle({type: "command"}); + yield shownPromise; + + is(PanelUI.panel.getAttribute("panelopen"), "true", "Check that panel has panelopen attribute"); + is(PanelUI.panel.state, "open", "Check that panel state is 'open'"); + + let hiddenPromise = promisePanelHidden(window); + PanelUI.toggle({type: "command"}); + yield hiddenPromise; + + ok(!PanelUI.panel.hasAttribute("panelopen"), "Check that panel doesn't have the panelopen attribute"); + is(PanelUI.panel.state, "closed", "Check that panel state is 'closed'"); +}); diff --git a/browser/components/customizableui/test/browser_switch_to_customize_mode.js b/browser/components/customizableui/test/browser_switch_to_customize_mode.js new file mode 100644 index 000000000..459ea7a1c --- /dev/null +++ b/browser/components/customizableui/test/browser_switch_to_customize_mode.js @@ -0,0 +1,34 @@ +"use strict"; + +add_task(function*() { + yield startCustomizing(); + is(gBrowser.tabs.length, 2, "Should have 2 tabs"); + + let paletteKidCount = document.getElementById("customization-palette").childElementCount; + let nonCustomizingTab = gBrowser.tabContainer.querySelector("tab:not([customizemode=true])"); + let finishedCustomizing = BrowserTestUtils.waitForEvent(gNavToolbox, "aftercustomization"); + yield BrowserTestUtils.switchTab(gBrowser, nonCustomizingTab); + yield finishedCustomizing; + + let startedCount = 0; + let handler = e => startedCount++; + gNavToolbox.addEventListener("customizationstarting", handler); + yield startCustomizing(); + CustomizableUI.removeWidgetFromArea("home-button"); + yield gCustomizeMode.reset().catch(e => { + ok(false, "Threw an exception trying to reset after making modifications in customize mode: " + e); + }); + + let newKidCount = document.getElementById("customization-palette").childElementCount; + is(newKidCount, paletteKidCount, "Should have just as many items in the palette as before."); + yield endCustomizing(); + is(startedCount, 1, "Should have only started once"); + gNavToolbox.removeEventListener("customizationstarting", handler); + let customizableToolbars = document.querySelectorAll("toolbar[customizable=true]:not([autohide=true])"); + for (let toolbar of customizableToolbars) { + ok(!toolbar.hasAttribute("customizing"), "Toolbar " + toolbar.id + " is no longer customizing"); + } + let menuitem = document.getElementById("PanelUI-customize"); + isnot(menuitem.getAttribute("label"), menuitem.getAttribute("exitLabel"), "Should have exited successfully"); +}); + diff --git a/browser/components/customizableui/test/head.js b/browser/components/customizableui/test/head.js new file mode 100644 index 000000000..7b8d84e20 --- /dev/null +++ b/browser/components/customizableui/test/head.js @@ -0,0 +1,499 @@ +/* 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"; + +// Avoid leaks by using tmp for imports... +var tmp = {}; +Cu.import("resource://gre/modules/Promise.jsm", tmp); +Cu.import("resource:///modules/CustomizableUI.jsm", tmp); +Cu.import("resource://gre/modules/AppConstants.jsm", tmp); +var {Promise, CustomizableUI, AppConstants} = tmp; + +var EventUtils = {}; +Services.scriptloader.loadSubScript("chrome://mochikit/content/tests/SimpleTest/EventUtils.js", EventUtils); + +Services.prefs.setBoolPref("browser.uiCustomization.skipSourceNodeCheck", true); +registerCleanupFunction(() => Services.prefs.clearUserPref("browser.uiCustomization.skipSourceNodeCheck")); + +// Remove temporary e10s related new window options in customize ui, +// they break a lot of tests. +CustomizableUI.destroyWidget("e10s-button"); +CustomizableUI.removeWidgetFromArea("e10s-button"); + +var {synthesizeDragStart, synthesizeDrop} = EventUtils; + +const kNSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"; +const kTabEventFailureTimeoutInMs = 20000; + +function createDummyXULButton(id, label, win = window) { + let btn = document.createElementNS(kNSXUL, "toolbarbutton"); + btn.id = id; + btn.setAttribute("label", label || id); + btn.className = "toolbarbutton-1 chromeclass-toolbar-additional"; + win.gNavToolbox.palette.appendChild(btn); + return btn; +} + +var gAddedToolbars = new Set(); + +function createToolbarWithPlacements(id, placements = []) { + gAddedToolbars.add(id); + let tb = document.createElementNS(kNSXUL, "toolbar"); + tb.id = id; + tb.setAttribute("customizable", "true"); + CustomizableUI.registerArea(id, { + type: CustomizableUI.TYPE_TOOLBAR, + defaultPlacements: placements + }); + gNavToolbox.appendChild(tb); + return tb; +} + +function createOverflowableToolbarWithPlacements(id, placements) { + gAddedToolbars.add(id); + + let tb = document.createElementNS(kNSXUL, "toolbar"); + tb.id = id; + tb.setAttribute("customizationtarget", id + "-target"); + + let customizationtarget = document.createElementNS(kNSXUL, "hbox"); + customizationtarget.id = id + "-target"; + customizationtarget.setAttribute("flex", "1"); + tb.appendChild(customizationtarget); + + let overflowPanel = document.createElementNS(kNSXUL, "panel"); + overflowPanel.id = id + "-overflow"; + document.getElementById("mainPopupSet").appendChild(overflowPanel); + + let overflowList = document.createElementNS(kNSXUL, "vbox"); + overflowList.id = id + "-overflow-list"; + overflowPanel.appendChild(overflowList); + + let chevron = document.createElementNS(kNSXUL, "toolbarbutton"); + chevron.id = id + "-chevron"; + tb.appendChild(chevron); + + CustomizableUI.registerArea(id, { + type: CustomizableUI.TYPE_TOOLBAR, + defaultPlacements: placements, + overflowable: true, + }); + + tb.setAttribute("customizable", "true"); + tb.setAttribute("overflowable", "true"); + tb.setAttribute("overflowpanel", overflowPanel.id); + tb.setAttribute("overflowtarget", overflowList.id); + tb.setAttribute("overflowbutton", chevron.id); + + gNavToolbox.appendChild(tb); + return tb; +} + +function removeCustomToolbars() { + CustomizableUI.reset(); + for (let toolbarId of gAddedToolbars) { + CustomizableUI.unregisterArea(toolbarId, true); + let tb = document.getElementById(toolbarId); + if (tb.hasAttribute("overflowpanel")) { + let panel = document.getElementById(tb.getAttribute("overflowpanel")); + if (panel) + panel.remove(); + } + tb.remove(); + } + gAddedToolbars.clear(); +} + +function getToolboxCustomToolbarId(toolbarName) { + return "__customToolbar_" + toolbarName.replace(" ", "_"); +} + +function resetCustomization() { + return CustomizableUI.reset(); +} + +function isInDevEdition() { + return AppConstants.MOZ_DEV_EDITION; +} + +function removeDeveloperButtonIfDevEdition(areaPanelPlacements) { + if (isInDevEdition()) { + areaPanelPlacements.splice(areaPanelPlacements.indexOf("developer-button"), 1); + } +} + +function assertAreaPlacements(areaId, expectedPlacements) { + let actualPlacements = getAreaWidgetIds(areaId); + placementArraysEqual(areaId, actualPlacements, expectedPlacements); +} + +function placementArraysEqual(areaId, actualPlacements, expectedPlacements) { + is(actualPlacements.length, expectedPlacements.length, + "Area " + areaId + " should have " + expectedPlacements.length + " items."); + let minItems = Math.min(expectedPlacements.length, actualPlacements.length); + for (let i = 0; i < minItems; i++) { + if (typeof expectedPlacements[i] == "string") { + is(actualPlacements[i], expectedPlacements[i], + "Item " + i + " in " + areaId + " should match expectations."); + } else if (expectedPlacements[i] instanceof RegExp) { + ok(expectedPlacements[i].test(actualPlacements[i]), + "Item " + i + " (" + actualPlacements[i] + ") in " + + areaId + " should match " + expectedPlacements[i]); + } else { + ok(false, "Unknown type of expected placement passed to " + + " assertAreaPlacements. Is your test broken?"); + } + } +} + +function todoAssertAreaPlacements(areaId, expectedPlacements) { + let actualPlacements = getAreaWidgetIds(areaId); + let isPassing = actualPlacements.length == expectedPlacements.length; + let minItems = Math.min(expectedPlacements.length, actualPlacements.length); + for (let i = 0; i < minItems; i++) { + if (typeof expectedPlacements[i] == "string") { + isPassing = isPassing && actualPlacements[i] == expectedPlacements[i]; + } else if (expectedPlacements[i] instanceof RegExp) { + isPassing = isPassing && expectedPlacements[i].test(actualPlacements[i]); + } else { + ok(false, "Unknown type of expected placement passed to " + + " assertAreaPlacements. Is your test broken?"); + } + } + todo(isPassing, "The area placements for " + areaId + + " should equal the expected placements."); +} + +function getAreaWidgetIds(areaId) { + return CustomizableUI.getWidgetIdsInArea(areaId); +} + +function simulateItemDrag(aToDrag, aTarget) { + synthesizeDrop(aToDrag.parentNode, aTarget); +} + +function endCustomizing(aWindow=window) { + if (aWindow.document.documentElement.getAttribute("customizing") != "true") { + return true; + } + Services.prefs.setBoolPref("browser.uiCustomization.disableAnimation", true); + let deferredEndCustomizing = Promise.defer(); + function onCustomizationEnds() { + Services.prefs.setBoolPref("browser.uiCustomization.disableAnimation", false); + aWindow.gNavToolbox.removeEventListener("aftercustomization", onCustomizationEnds); + deferredEndCustomizing.resolve(); + } + aWindow.gNavToolbox.addEventListener("aftercustomization", onCustomizationEnds); + aWindow.gCustomizeMode.exit(); + + return deferredEndCustomizing.promise; +} + +function startCustomizing(aWindow=window) { + if (aWindow.document.documentElement.getAttribute("customizing") == "true") { + return null; + } + Services.prefs.setBoolPref("browser.uiCustomization.disableAnimation", true); + let deferred = Promise.defer(); + function onCustomizing() { + aWindow.gNavToolbox.removeEventListener("customizationready", onCustomizing); + Services.prefs.setBoolPref("browser.uiCustomization.disableAnimation", false); + deferred.resolve(); + } + aWindow.gNavToolbox.addEventListener("customizationready", onCustomizing); + aWindow.gCustomizeMode.enter(); + return deferred.promise; +} + +function promiseObserverNotified(aTopic) { + let deferred = Promise.defer(); + Services.obs.addObserver(function onNotification(aSubject, aTopic, aData) { + Services.obs.removeObserver(onNotification, aTopic); + deferred.resolve({subject: aSubject, data: aData}); + }, aTopic, false); + return deferred.promise; +} + +function openAndLoadWindow(aOptions, aWaitForDelayedStartup=false) { + let deferred = Promise.defer(); + let win = OpenBrowserWindow(aOptions); + if (aWaitForDelayedStartup) { + Services.obs.addObserver(function onDS(aSubject, aTopic, aData) { + if (aSubject != win) { + return; + } + Services.obs.removeObserver(onDS, "browser-delayed-startup-finished"); + deferred.resolve(win); + }, "browser-delayed-startup-finished", false); + + } else { + win.addEventListener("load", function onLoad() { + win.removeEventListener("load", onLoad); + deferred.resolve(win); + }); + } + return deferred.promise; +} + +function promiseWindowClosed(win) { + let deferred = Promise.defer(); + win.addEventListener("unload", function onunload() { + win.removeEventListener("unload", onunload); + deferred.resolve(); + }); + win.close(); + return deferred.promise; +} + +function promisePanelShown(win) { + let panelEl = win.PanelUI.panel; + return promisePanelElementShown(win, panelEl); +} + +function promiseOverflowShown(win) { + let panelEl = win.document.getElementById("widget-overflow"); + return promisePanelElementShown(win, panelEl); +} + +function promisePanelElementShown(win, aPanel) { + let deferred = Promise.defer(); + let timeoutId = win.setTimeout(() => { + deferred.reject("Panel did not show within 20 seconds."); + }, 20000); + function onPanelOpen(e) { + aPanel.removeEventListener("popupshown", onPanelOpen); + win.clearTimeout(timeoutId); + deferred.resolve(); + } + aPanel.addEventListener("popupshown", onPanelOpen); + return deferred.promise; +} + +function promisePanelHidden(win) { + let panelEl = win.PanelUI.panel; + return promisePanelElementHidden(win, panelEl); +} + +function promiseOverflowHidden(win) { + let panelEl = document.getElementById("widget-overflow"); + return promisePanelElementHidden(win, panelEl); +} + +function promisePanelElementHidden(win, aPanel) { + let deferred = Promise.defer(); + let timeoutId = win.setTimeout(() => { + deferred.reject("Panel did not hide within 20 seconds."); + }, 20000); + function onPanelClose(e) { + aPanel.removeEventListener("popuphidden", onPanelClose); + win.clearTimeout(timeoutId); + deferred.resolve(); + } + aPanel.addEventListener("popuphidden", onPanelClose); + return deferred.promise; +} + +function isPanelUIOpen() { + return PanelUI.panel.state == "open" || PanelUI.panel.state == "showing"; +} + +function subviewShown(aSubview) { + let deferred = Promise.defer(); + let win = aSubview.ownerGlobal; + let timeoutId = win.setTimeout(() => { + deferred.reject("Subview (" + aSubview.id + ") did not show within 20 seconds."); + }, 20000); + function onViewShowing(e) { + aSubview.removeEventListener("ViewShowing", onViewShowing); + win.clearTimeout(timeoutId); + deferred.resolve(); + } + aSubview.addEventListener("ViewShowing", onViewShowing); + return deferred.promise; +} + +function subviewHidden(aSubview) { + let deferred = Promise.defer(); + let win = aSubview.ownerGlobal; + let timeoutId = win.setTimeout(() => { + deferred.reject("Subview (" + aSubview.id + ") did not hide within 20 seconds."); + }, 20000); + function onViewHiding(e) { + aSubview.removeEventListener("ViewHiding", onViewHiding); + win.clearTimeout(timeoutId); + deferred.resolve(); + } + aSubview.addEventListener("ViewHiding", onViewHiding); + return deferred.promise; +} + +function waitForCondition(aConditionFn, aMaxTries=50, aCheckInterval=100) { + function tryNow() { + tries++; + if (aConditionFn()) { + deferred.resolve(); + } else if (tries < aMaxTries) { + tryAgain(); + } else { + deferred.reject("Condition timed out: " + aConditionFn.toSource()); + } + } + function tryAgain() { + setTimeout(tryNow, aCheckInterval); + } + let deferred = Promise.defer(); + let tries = 0; + tryAgain(); + return deferred.promise; +} + +function waitFor(aTimeout=100) { + let deferred = Promise.defer(); + setTimeout(() => deferred.resolve(), aTimeout); + return deferred.promise; +} + +/** + * Starts a load in an existing tab and waits for it to finish (via some event). + * + * @param aTab The tab to load into. + * @param aUrl The url to load. + * @param aEventType The load event type to wait for. Defaults to "load". + * @return {Promise} resolved when the event is handled. + */ +function promiseTabLoadEvent(aTab, aURL) { + let browser = aTab.linkedBrowser; + + BrowserTestUtils.loadURI(browser, aURL); + return BrowserTestUtils.browserLoaded(browser); +} + +/** + * Navigate back or forward in tab history and wait for it to finish. + * + * @param aDirection Number to indicate to move backward or forward in history. + * @param aConditionFn Function that returns the result of an evaluated condition + * that needs to be `true` to resolve the promise. + * @return {Promise} resolved when navigation has finished. + */ +function promiseTabHistoryNavigation(aDirection = -1, aConditionFn) { + let deferred = Promise.defer(); + + let timeoutId = setTimeout(() => { + gBrowser.removeEventListener("pageshow", listener, true); + deferred.reject("Pageshow did not happen within " + kTabEventFailureTimeoutInMs + "ms"); + }, kTabEventFailureTimeoutInMs); + + function listener(event) { + gBrowser.removeEventListener("pageshow", listener, true); + clearTimeout(timeoutId); + + if (aConditionFn) { + waitForCondition(aConditionFn).then(() => deferred.resolve(), + aReason => deferred.reject(aReason)); + } else { + deferred.resolve(); + } + } + gBrowser.addEventListener("pageshow", listener, true); + + content.history.go(aDirection); + + return deferred.promise; +} + +/** + * Wait for an attribute on a node to change + * + * @param aNode Node on which the mutation is expected + * @param aAttribute The attribute we're interested in + * @param aFilterFn A function to check if the new value is what we want. + * @return {Promise} resolved when the requisite mutation shows up. + */ +function promiseAttributeMutation(aNode, aAttribute, aFilterFn) { + return new Promise((resolve, reject) => { + info("waiting for mutation of attribute '" + aAttribute + "'."); + let obs = new MutationObserver((mutations) => { + for (let mut of mutations) { + let attr = mut.attributeName; + let newValue = mut.target.getAttribute(attr); + if (aFilterFn(newValue)) { + ok(true, "mutation occurred: attribute '" + attr + "' changed to '" + newValue + "' from '" + mut.oldValue + "'."); + obs.disconnect(); + resolve(); + } else { + info("Ignoring mutation that produced value " + newValue + " because of filter."); + } + } + }); + obs.observe(aNode, {attributeFilter: [aAttribute]}); + }); +} + +function popupShown(aPopup) { + return promisePopupEvent(aPopup, "shown"); +} + +function popupHidden(aPopup) { + return promisePopupEvent(aPopup, "hidden"); +} + +/** + * Returns a Promise that resolves when aPopup fires an event of type + * aEventType. Times out and rejects after 20 seconds. + * + * @param aPopup the popup to monitor for events. + * @param aEventSuffix the _suffix_ for the popup event type to watch for. + * + * Example usage: + * let popupShownPromise = promisePopupEvent(somePopup, "shown"); + * // ... something that opens a popup + * yield popupShownPromise; + * + * let popupHiddenPromise = promisePopupEvent(somePopup, "hidden"); + * // ... something that hides a popup + * yield popupHiddenPromise; + */ +function promisePopupEvent(aPopup, aEventSuffix) { + let deferred = Promise.defer(); + let eventType = "popup" + aEventSuffix; + + function onPopupEvent(e) { + aPopup.removeEventListener(eventType, onPopupEvent); + deferred.resolve(); + } + + aPopup.addEventListener(eventType, onPopupEvent); + return deferred.promise; +} + +// This is a simpler version of the context menu check that +// exists in contextmenu_common.js. +function checkContextMenu(aContextMenu, aExpectedEntries, aWindow=window) { + let childNodes = [...aContextMenu.childNodes]; + // Ignore hidden nodes: + childNodes = childNodes.filter((n) => !n.hidden); + + for (let i = 0; i < childNodes.length; i++) { + let menuitem = childNodes[i]; + try { + if (aExpectedEntries[i][0] == "---") { + is(menuitem.localName, "menuseparator", "menuseparator expected"); + continue; + } + + let selector = aExpectedEntries[i][0]; + ok(menuitem.matches(selector), "menuitem should match " + selector + " selector"); + let commandValue = menuitem.getAttribute("command"); + let relatedCommand = commandValue ? aWindow.document.getElementById(commandValue) : null; + let menuItemDisabled = relatedCommand ? + relatedCommand.getAttribute("disabled") == "true" : + menuitem.getAttribute("disabled") == "true"; + is(menuItemDisabled, !aExpectedEntries[i][1], "disabled state for " + selector); + } catch (e) { + ok(false, "Exception when checking context menu: " + e); + } + } +} diff --git a/browser/components/customizableui/test/support/feeds_test_page.html b/browser/components/customizableui/test/support/feeds_test_page.html new file mode 100644 index 000000000..be78e4dff --- /dev/null +++ b/browser/components/customizableui/test/support/feeds_test_page.html @@ -0,0 +1,10 @@ +<html> +<head> + <title>Feeds test page</title> + <link rel="alternate" type="application/rss+xml" href="test-feed.xml" title="Test feed"> +</head> + +<body> + This is a test page for feeds +</body> +</html> diff --git a/browser/components/customizableui/test/support/test-feed.xml b/browser/components/customizableui/test/support/test-feed.xml new file mode 100644 index 000000000..0e700b6d8 --- /dev/null +++ b/browser/components/customizableui/test/support/test-feed.xml @@ -0,0 +1,23 @@ +<?xml version="1.0" encoding="utf-8"?> +<feed xmlns="http://www.w3.org/2005/Atom"> + + <title>Example Feed</title> + <link href="http://example.org/"/> + <updated>2010-08-22T18:30:02Z</updated> + + <author> + <name>John Doe</name> + </author> + <id>urn:uuid:e2df8375-99be-4848-b05e-b9d407555267</id> + + <entry> + + <title>Item</title> + <link href="http://example.org/first"/> + <id>urn:uuid:9e0f4bed-33d3-4a9d-97ab-ecaa31b3f14a</id> + <updated>2010-08-22T18:30:02Z</updated> + + <summary>Some text.</summary> + </entry> + +</feed> diff --git a/browser/components/customizableui/test/support/test_967000_charEncoding_page.html b/browser/components/customizableui/test/support/test_967000_charEncoding_page.html new file mode 100644 index 000000000..c8d35115c --- /dev/null +++ b/browser/components/customizableui/test/support/test_967000_charEncoding_page.html @@ -0,0 +1,11 @@ +<!DOCTYPE html>
+<html>
+ <head>
+ <meta charset="utf-8">
+ <title>Test page</title>
+ </head>
+
+ <body>
+ This is a test page
+ </body>
+</html>
|