summaryrefslogtreecommitdiffstats
path: root/browser/components/customizableui
diff options
context:
space:
mode:
Diffstat (limited to 'browser/components/customizableui')
-rw-r--r--browser/components/customizableui/CustomizableUI.jsm4420
-rw-r--r--browser/components/customizableui/CustomizableWidgets.jsm1281
-rw-r--r--browser/components/customizableui/CustomizeMode.jsm2341
-rw-r--r--browser/components/customizableui/DragPositionManager.jsm420
-rw-r--r--browser/components/customizableui/PanelWideWidgetTracker.jsm172
-rw-r--r--browser/components/customizableui/ScrollbarSampler.jsm65
-rw-r--r--browser/components/customizableui/content/customizeMode.inc.xul82
-rw-r--r--browser/components/customizableui/content/jar.mn10
-rw-r--r--browser/components/customizableui/content/moz.build7
-rw-r--r--browser/components/customizableui/content/panelUI.css31
-rw-r--r--browser/components/customizableui/content/panelUI.inc.xul407
-rw-r--r--browser/components/customizableui/content/panelUI.js558
-rw-r--r--browser/components/customizableui/content/panelUI.xml509
-rw-r--r--browser/components/customizableui/content/toolbar.xml618
-rw-r--r--browser/components/customizableui/moz.build26
-rw-r--r--browser/components/customizableui/test/.eslintrc.js7
-rw-r--r--browser/components/customizableui/test/browser.ini154
-rw-r--r--browser/components/customizableui/test/browser_1003588_no_specials_in_panel.js107
-rw-r--r--browser/components/customizableui/test/browser_1007336_lwthemes_in_customize_mode.js108
-rw-r--r--browser/components/customizableui/test/browser_1008559_anchor_undo_restore.js71
-rw-r--r--browser/components/customizableui/test/browser_1042100_default_placements_update.js107
-rw-r--r--browser/components/customizableui/test/browser_1058573_showToolbarsDropdown.js25
-rw-r--r--browser/components/customizableui/test/browser_1087303_button_fullscreen.js46
-rw-r--r--browser/components/customizableui/test/browser_1087303_button_preferences.js50
-rw-r--r--browser/components/customizableui/test/browser_1089591_still_customizable_after_reset.js24
-rw-r--r--browser/components/customizableui/test/browser_1096763_seen_widgets_post_reset.js31
-rw-r--r--browser/components/customizableui/test/browser_1161838_inserted_new_default_buttons.js78
-rw-r--r--browser/components/customizableui/test/browser_873501_handle_specials.js79
-rw-r--r--browser/components/customizableui/test/browser_876926_customize_mode_wrapping.js185
-rw-r--r--browser/components/customizableui/test/browser_876944_customize_mode_create_destroy.js61
-rw-r--r--browser/components/customizableui/test/browser_877006_missing_view.js41
-rw-r--r--browser/components/customizableui/test/browser_877178_unregisterArea.js50
-rw-r--r--browser/components/customizableui/test/browser_877447_skip_missing_ids.js25
-rw-r--r--browser/components/customizableui/test/browser_878452_drag_to_panel.js65
-rw-r--r--browser/components/customizableui/test/browser_880164_customization_context_menus.js414
-rw-r--r--browser/components/customizableui/test/browser_880382_drag_wide_widgets_in_panel.js497
-rw-r--r--browser/components/customizableui/test/browser_884402_customize_from_overflow.js81
-rw-r--r--browser/components/customizableui/test/browser_885052_customize_mode_observers_disabed.js45
-rw-r--r--browser/components/customizableui/test/browser_885530_showInPrivateBrowsing.js134
-rw-r--r--browser/components/customizableui/test/browser_886323_buildArea_removable_nodes.js46
-rw-r--r--browser/components/customizableui/test/browser_887438_currentset_shim.js75
-rw-r--r--browser/components/customizableui/test/browser_888817_currentset_updating.js57
-rw-r--r--browser/components/customizableui/test/browser_890140_orphaned_placeholders.js210
-rw-r--r--browser/components/customizableui/test/browser_890262_destroyWidget_after_add_to_panel.js68
-rw-r--r--browser/components/customizableui/test/browser_892955_isWidgetRemovable_for_removed_widgets.js30
-rw-r--r--browser/components/customizableui/test/browser_892956_destroyWidget_defaultPlacements.js24
-rw-r--r--browser/components/customizableui/test/browser_901207_searchbar_in_panel.js113
-rw-r--r--browser/components/customizableui/test/browser_909779_overflow_toolbars_new_window.js31
-rw-r--r--browser/components/customizableui/test/browser_913972_currentset_overflow.js55
-rw-r--r--browser/components/customizableui/test/browser_914138_widget_API_overflowable_toolbar.js131
-rw-r--r--browser/components/customizableui/test/browser_914863_disabled_help_quit_buttons.js16
-rw-r--r--browser/components/customizableui/test/browser_918049_skipintoolbarset_dnd.js38
-rw-r--r--browser/components/customizableui/test/browser_923857_customize_mode_event_wrapping_during_reset.js24
-rw-r--r--browser/components/customizableui/test/browser_927717_customize_drag_empty_toolbar.js26
-rw-r--r--browser/components/customizableui/test/browser_932928_show_notice_when_palette_empty.js35
-rw-r--r--browser/components/customizableui/test/browser_934113_menubar_removable.js30
-rw-r--r--browser/components/customizableui/test/browser_934951_zoom_in_toolbar.js89
-rw-r--r--browser/components/customizableui/test/browser_938980_navbar_collapsed.js121
-rw-r--r--browser/components/customizableui/test/browser_938995_indefaultstate_nonremovable.js25
-rw-r--r--browser/components/customizableui/test/browser_940013_registerToolbarNode_calls_registerArea.js70
-rw-r--r--browser/components/customizableui/test/browser_940307_panel_click_closure_handling.js136
-rw-r--r--browser/components/customizableui/test/browser_940946_removable_from_navbar_customizemode.js22
-rw-r--r--browser/components/customizableui/test/browser_941083_invalidate_wrapper_cache_createWidget.js31
-rw-r--r--browser/components/customizableui/test/browser_942581_unregisterArea_keeps_placements.js106
-rw-r--r--browser/components/customizableui/test/browser_943683_migration_test.js50
-rw-r--r--browser/components/customizableui/test/browser_944887_destroyWidget_should_destroy_in_palette.js17
-rw-r--r--browser/components/customizableui/test/browser_945739_showInPrivateBrowsing_customize_mode.js35
-rw-r--r--browser/components/customizableui/test/browser_947914_button_addons.js33
-rw-r--r--browser/components/customizableui/test/browser_947914_button_copy.js59
-rw-r--r--browser/components/customizableui/test/browser_947914_button_cut.js57
-rw-r--r--browser/components/customizableui/test/browser_947914_button_find.js22
-rw-r--r--browser/components/customizableui/test/browser_947914_button_history.js24
-rw-r--r--browser/components/customizableui/test/browser_947914_button_newPrivateWindow.js48
-rw-r--r--browser/components/customizableui/test/browser_947914_button_newWindow.js47
-rw-r--r--browser/components/customizableui/test/browser_947914_button_paste.js41
-rw-r--r--browser/components/customizableui/test/browser_947914_button_print.js45
-rw-r--r--browser/components/customizableui/test/browser_947914_button_savePage.js20
-rw-r--r--browser/components/customizableui/test/browser_947914_button_zoomIn.js37
-rw-r--r--browser/components/customizableui/test/browser_947914_button_zoomOut.js38
-rw-r--r--browser/components/customizableui/test/browser_947914_button_zoomReset.js40
-rw-r--r--browser/components/customizableui/test/browser_947987_removable_default.js68
-rw-r--r--browser/components/customizableui/test/browser_948985_non_removable_defaultArea.js32
-rw-r--r--browser/components/customizableui/test/browser_952963_areaType_getter_no_area.js52
-rw-r--r--browser/components/customizableui/test/browser_956602_remove_special_widget.js31
-rw-r--r--browser/components/customizableui/test/browser_962069_drag_to_overflow_chevron.js54
-rw-r--r--browser/components/customizableui/test/browser_962884_opt_in_disable_hyphens.js67
-rw-r--r--browser/components/customizableui/test/browser_963639_customizing_attribute_non_customizable_toolbar.js34
-rw-r--r--browser/components/customizableui/test/browser_967000_button_charEncoding.js62
-rw-r--r--browser/components/customizableui/test/browser_967000_button_feeds.js60
-rw-r--r--browser/components/customizableui/test/browser_967000_button_sync.js335
-rw-r--r--browser/components/customizableui/test/browser_968447_bookmarks_toolbar_items_in_panel.js65
-rw-r--r--browser/components/customizableui/test/browser_968565_insert_before_hidden_items.js56
-rw-r--r--browser/components/customizableui/test/browser_969427_recreate_destroyed_widget_after_reset.js34
-rw-r--r--browser/components/customizableui/test/browser_969661_character_encoding_navbar_disabled.js26
-rw-r--r--browser/components/customizableui/test/browser_970511_undo_restore_default.js128
-rw-r--r--browser/components/customizableui/test/browser_972267_customizationchange_events.js46
-rwxr-xr-xbrowser/components/customizableui/test/browser_973641_button_addon.js71
-rw-r--r--browser/components/customizableui/test/browser_973932_addonbar_currentset.js30
-rw-r--r--browser/components/customizableui/test/browser_975719_customtoolbars_behaviour.js145
-rw-r--r--browser/components/customizableui/test/browser_976792_insertNodeInWindow.js414
-rw-r--r--browser/components/customizableui/test/browser_978084_dragEnd_after_move.js46
-rw-r--r--browser/components/customizableui/test/browser_980155_add_overflow_toolbar.js51
-rw-r--r--browser/components/customizableui/test/browser_981305_separator_insertion.js73
-rw-r--r--browser/components/customizableui/test/browser_981418-widget-onbeforecreated-handler.js93
-rw-r--r--browser/components/customizableui/test/browser_982656_restore_defaults_builtin_widgets.js57
-rw-r--r--browser/components/customizableui/test/browser_984455_bookmarks_items_reparenting.js267
-rw-r--r--browser/components/customizableui/test/browser_985815_propagate_setToolbarVisibility.js45
-rw-r--r--browser/components/customizableui/test/browser_987177_destroyWidget_xul.js33
-rw-r--r--browser/components/customizableui/test/browser_987177_xul_wrapper_updating.js74
-rwxr-xr-xbrowser/components/customizableui/test/browser_987185_syncButton.js77
-rw-r--r--browser/components/customizableui/test/browser_987492_window_api.js54
-rw-r--r--browser/components/customizableui/test/browser_987640_charEncoding.js60
-rw-r--r--browser/components/customizableui/test/browser_988072_sidebar_events.js392
-rw-r--r--browser/components/customizableui/test/browser_989338_saved_placements_not_resaved.js56
-rw-r--r--browser/components/customizableui/test/browser_989751_subviewbutton_class.js62
-rw-r--r--browser/components/customizableui/test/browser_992747_toggle_noncustomizable_toolbar.js26
-rw-r--r--browser/components/customizableui/test/browser_993322_widget_notoolbar.js36
-rw-r--r--browser/components/customizableui/test/browser_995164_registerArea_during_customize_mode.js149
-rw-r--r--browser/components/customizableui/test/browser_996364_registerArea_different_properties.js112
-rw-r--r--browser/components/customizableui/test/browser_996635_remove_non_widgets.js43
-rw-r--r--browser/components/customizableui/test/browser_bootstrapped_custom_toolbar.js81
-rw-r--r--browser/components/customizableui/test/browser_check_tooltips_in_navbar.js14
-rw-r--r--browser/components/customizableui/test/browser_customizemode_contextmenu_menubuttonstate.js24
-rw-r--r--browser/components/customizableui/test/browser_panel_toggle.js43
-rw-r--r--browser/components/customizableui/test/browser_switch_to_customize_mode.js34
-rw-r--r--browser/components/customizableui/test/head.js499
-rw-r--r--browser/components/customizableui/test/support/feeds_test_page.html10
-rw-r--r--browser/components/customizableui/test/support/test-feed.xml23
-rw-r--r--browser/components/customizableui/test/support/test_967000_charEncoding_page.html11
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 &shy; 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 &shy; 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 &shy; 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 &shy; 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 &shy; 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>