3045 lines
96 KiB
JavaScript
3045 lines
96 KiB
JavaScript
/* 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 kPrefCustomizationDebug = "browser.uiCustomization.debug";
|
|
const kPaletteId = "customization-palette";
|
|
const kDragDataTypePrefix = "text/toolbarwrapper-id/";
|
|
const kSkipSourceNodePref = "browser.uiCustomization.skipSourceNodeCheck";
|
|
const kDrawInTitlebarPref = "browser.tabs.inTitlebar";
|
|
const kCompactModeShowPref = "browser.compactmode.show";
|
|
const kBookmarksToolbarPref = "browser.toolbars.bookmarks.visibility";
|
|
const kKeepBroadcastAttributes = "keepbroadcastattributeswhencustomizing";
|
|
|
|
const kPanelItemContextMenu = "customizationPanelItemContextMenu";
|
|
const kPaletteItemContextMenu = "customizationPaletteItemContextMenu";
|
|
|
|
const kDownloadAutohideCheckboxId = "downloads-button-autohide-checkbox";
|
|
const kDownloadAutohidePanelId = "downloads-button-autohide-panel";
|
|
const kDownloadAutoHidePref = "browser.download.autohideButton";
|
|
|
|
import { CustomizableUI } from "resource:///modules/CustomizableUI.sys.mjs";
|
|
import { XPCOMUtils } from "resource://gre/modules/XPCOMUtils.sys.mjs";
|
|
import { AppConstants } from "resource://gre/modules/AppConstants.sys.mjs";
|
|
|
|
const lazy = {};
|
|
|
|
ChromeUtils.defineESModuleGetters(lazy, {
|
|
AddonManager: "resource://gre/modules/AddonManager.sys.mjs",
|
|
BrowserUsageTelemetry: "resource:///modules/BrowserUsageTelemetry.sys.mjs",
|
|
DragPositionManager: "resource:///modules/DragPositionManager.sys.mjs",
|
|
URILoadingHelper: "resource:///modules/URILoadingHelper.sys.mjs",
|
|
});
|
|
ChromeUtils.defineLazyGetter(lazy, "gWidgetsBundle", function () {
|
|
const kUrl =
|
|
"chrome://browser/locale/customizableui/customizableWidgets.properties";
|
|
return Services.strings.createBundle(kUrl);
|
|
});
|
|
XPCOMUtils.defineLazyServiceGetter(
|
|
lazy,
|
|
"gTouchBarUpdater",
|
|
"@mozilla.org/widget/touchbarupdater;1",
|
|
"nsITouchBarUpdater"
|
|
);
|
|
|
|
let gDebug;
|
|
ChromeUtils.defineLazyGetter(lazy, "log", () => {
|
|
let { ConsoleAPI } = ChromeUtils.importESModule(
|
|
"resource://gre/modules/Console.sys.mjs"
|
|
);
|
|
gDebug = Services.prefs.getBoolPref(kPrefCustomizationDebug, false);
|
|
let consoleOptions = {
|
|
maxLogLevel: gDebug ? "all" : "log",
|
|
prefix: "CustomizeMode",
|
|
};
|
|
return new ConsoleAPI(consoleOptions);
|
|
});
|
|
|
|
var gDraggingInToolbars;
|
|
|
|
var gTab;
|
|
|
|
function closeGlobalTab() {
|
|
let win = gTab.ownerGlobal;
|
|
if (win.gBrowser.browsers.length == 1) {
|
|
win.BrowserCommands.openTab();
|
|
}
|
|
win.gBrowser.removeTab(gTab, { animate: true });
|
|
gTab = null;
|
|
}
|
|
|
|
var gTabsProgressListener = {
|
|
onLocationChange(aBrowser, aWebProgress, aRequest, aLocation) {
|
|
// Tear down customize mode when the customize mode tab loads some other page.
|
|
// Customize mode will be re-entered if "about:blank" is loaded again, so
|
|
// don't tear down in this case.
|
|
if (
|
|
!gTab ||
|
|
gTab.linkedBrowser != aBrowser ||
|
|
aLocation.spec == "about:blank"
|
|
) {
|
|
return;
|
|
}
|
|
|
|
unregisterGlobalTab();
|
|
},
|
|
};
|
|
|
|
function unregisterGlobalTab() {
|
|
gTab.removeEventListener("TabClose", unregisterGlobalTab);
|
|
let win = gTab.ownerGlobal;
|
|
win.removeEventListener("unload", unregisterGlobalTab);
|
|
win.gBrowser.removeTabsProgressListener(gTabsProgressListener);
|
|
|
|
gTab.removeAttribute("customizemode");
|
|
|
|
gTab = null;
|
|
}
|
|
|
|
export function CustomizeMode(aWindow) {
|
|
this.window = aWindow;
|
|
this.document = aWindow.document;
|
|
this.browser = aWindow.gBrowser;
|
|
this.areas = new Set();
|
|
|
|
this._translationObserver = new aWindow.MutationObserver(mutations =>
|
|
this._onTranslations(mutations)
|
|
);
|
|
this._ensureCustomizationPanels();
|
|
|
|
let content = this.$("customization-content-container");
|
|
if (!content) {
|
|
this.window.MozXULElement.insertFTLIfNeeded("browser/customizeMode.ftl");
|
|
let container = this.$("customization-container");
|
|
container.replaceChild(
|
|
this.window.MozXULElement.parseXULToFragment(container.firstChild.data),
|
|
container.lastChild
|
|
);
|
|
}
|
|
|
|
this._attachEventListeners();
|
|
|
|
// There are two palettes - there's the palette that can be overlayed with
|
|
// toolbar items in browser.xhtml. 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.$(kPaletteId);
|
|
this.pongArena = this.$("customization-pong-arena");
|
|
|
|
if (this._canDrawInTitlebar()) {
|
|
this._updateTitlebarCheckbox();
|
|
Services.prefs.addObserver(kDrawInTitlebarPref, this);
|
|
} else {
|
|
this.$("customization-titlebar-visibility-checkbox").hidden = true;
|
|
}
|
|
|
|
// Observe pref changes to the bookmarks toolbar visibility,
|
|
// since we won't get a toolbarvisibilitychange event if the
|
|
// toolbar is changing from 'newtab' to 'always' in Customize mode
|
|
// since the toolbar is shown with the 'newtab' setting.
|
|
Services.prefs.addObserver(kBookmarksToolbarPref, this);
|
|
|
|
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,
|
|
|
|
// These are the commands we continue to leave enabled while in customize mode.
|
|
// All other commands are disabled, and we remove the disabled attribute when
|
|
// leaving customize mode.
|
|
_enabledCommands: new Set([
|
|
"cmd_newNavigator",
|
|
"cmd_newNavigatorTab",
|
|
"cmd_newNavigatorTabNoEvent",
|
|
"cmd_close",
|
|
"cmd_closeWindow",
|
|
"cmd_quitApplication",
|
|
"View:FullScreen",
|
|
"Browser:NextTab",
|
|
"Browser:PrevTab",
|
|
"Browser:NewUserContextTab",
|
|
"Tools:PrivateBrowsing",
|
|
"minimizeWindow",
|
|
"zoomWindow",
|
|
]),
|
|
|
|
get _handler() {
|
|
return this.window.CustomizationHandler;
|
|
},
|
|
|
|
uninit() {
|
|
if (this._canDrawInTitlebar()) {
|
|
Services.prefs.removeObserver(kDrawInTitlebarPref, this);
|
|
}
|
|
Services.prefs.removeObserver(kBookmarksToolbarPref, this);
|
|
},
|
|
|
|
$(id) {
|
|
return this.document.getElementById(id);
|
|
},
|
|
|
|
toggle() {
|
|
if (
|
|
this._handler.isEnteringCustomizeMode ||
|
|
this._handler.isExitingCustomizeMode
|
|
) {
|
|
this._wantToBeInCustomizeMode = !this._wantToBeInCustomizeMode;
|
|
return;
|
|
}
|
|
if (this._customizing) {
|
|
this.exit();
|
|
} else {
|
|
this.enter();
|
|
}
|
|
},
|
|
|
|
setTab(aTab) {
|
|
if (gTab == aTab) {
|
|
return;
|
|
}
|
|
|
|
if (gTab) {
|
|
closeGlobalTab();
|
|
}
|
|
|
|
gTab = aTab;
|
|
|
|
gTab.setAttribute("customizemode", "true");
|
|
|
|
if (gTab.linkedPanel) {
|
|
gTab.linkedBrowser.stop();
|
|
}
|
|
|
|
let win = gTab.ownerGlobal;
|
|
|
|
win.gBrowser.setTabTitle(gTab);
|
|
win.gBrowser.setIcon(gTab, "chrome://browser/skin/customize.svg");
|
|
|
|
gTab.addEventListener("TabClose", unregisterGlobalTab);
|
|
|
|
win.gBrowser.addTabsProgressListener(gTabsProgressListener);
|
|
|
|
win.addEventListener("unload", unregisterGlobalTab);
|
|
|
|
if (gTab.selected) {
|
|
win.gCustomizeMode.enter();
|
|
}
|
|
},
|
|
|
|
enter() {
|
|
if (!this.window.toolbar.visible) {
|
|
let w = lazy.URILoadingHelper.getTargetWindow(this.window, {
|
|
skipPopups: true,
|
|
});
|
|
if (w) {
|
|
w.gCustomizeMode.enter();
|
|
return;
|
|
}
|
|
let obs = () => {
|
|
Services.obs.removeObserver(obs, "browser-delayed-startup-finished");
|
|
w = lazy.URILoadingHelper.getTargetWindow(this.window, {
|
|
skipPopups: true,
|
|
});
|
|
w.gCustomizeMode.enter();
|
|
};
|
|
Services.obs.addObserver(obs, "browser-delayed-startup-finished");
|
|
this.window.openTrustedLinkIn("about:newtab", "window");
|
|
return;
|
|
}
|
|
this._wantToBeInCustomizeMode = true;
|
|
|
|
if (this._customizing || this._handler.isEnteringCustomizeMode) {
|
|
return;
|
|
}
|
|
|
|
// Exiting; want to re-enter once we've done that.
|
|
if (this._handler.isExitingCustomizeMode) {
|
|
lazy.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.addTab("about:blank", {
|
|
inBackground: false,
|
|
forceNotRemote: true,
|
|
skipAnimation: true,
|
|
triggeringPrincipal:
|
|
Services.scriptSecurityManager.getSystemPrincipal(),
|
|
})
|
|
);
|
|
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.$("customization-reset-button");
|
|
resetButton.setAttribute("disabled", "true");
|
|
|
|
(async () => {
|
|
// We shouldn't start customize mode until after browser-delayed-startup has finished:
|
|
if (!this.window.gBrowserInit.delayedStartupFinished) {
|
|
await 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"
|
|
);
|
|
});
|
|
}
|
|
|
|
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();
|
|
|
|
let panelHolder = document.getElementById("customization-panelHolder");
|
|
let panelContextMenu = document.getElementById(kPanelItemContextMenu);
|
|
this._previousPanelContextMenuParent = panelContextMenu.parentNode;
|
|
document.getElementById("mainPopupSet").appendChild(panelContextMenu);
|
|
panelHolder.appendChild(window.PanelUI.overflowFixedList);
|
|
|
|
window.PanelUI.overflowFixedList.toggleAttribute("customizing", true);
|
|
window.PanelUI.menuButton.disabled = true;
|
|
document.getElementById("nav-bar-overflow-button").disabled = true;
|
|
|
|
this._transitioning = true;
|
|
|
|
let customizer = document.getElementById("customization-container");
|
|
let browser = document.getElementById("browser");
|
|
browser.hidden = true;
|
|
customizer.hidden = false;
|
|
|
|
this._wrapToolbarItemSync(CustomizableUI.AREA_TABSTRIP);
|
|
|
|
this.document.documentElement.toggleAttribute("customizing", true);
|
|
|
|
let customizableToolbars = document.querySelectorAll(
|
|
"toolbar[customizable=true]:not([autohide=true], [collapsed=true])"
|
|
);
|
|
for (let toolbar of customizableToolbars) {
|
|
toolbar.toggleAttribute("customizing", true);
|
|
}
|
|
|
|
this._updateOverflowPanelArrowOffset();
|
|
|
|
// Let everybody in this window know that we're about to customize.
|
|
CustomizableUI.dispatchToolboxEvent("customizationstarting", {}, window);
|
|
|
|
await this._wrapToolbarItems();
|
|
this.populatePalette();
|
|
|
|
this._setupPaletteDragging();
|
|
|
|
window.gNavToolbox.addEventListener("toolbarvisibilitychange", this);
|
|
|
|
this._updateResetButton();
|
|
this._updateUndoResetButton();
|
|
this._updateTouchBarButton();
|
|
this._updateDensityMenu();
|
|
|
|
this._skipSourceNodeCheck =
|
|
Services.prefs.getPrefType(kSkipSourceNodePref) ==
|
|
Ci.nsIPrefBranch.PREF_BOOL &&
|
|
Services.prefs.getBoolPref(kSkipSourceNodePref);
|
|
|
|
CustomizableUI.addListener(this);
|
|
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();
|
|
|
|
lazy.AddonManager.addAddonListener(this);
|
|
|
|
this._setupDownloadAutoHideToggle();
|
|
|
|
this._handler.isEnteringCustomizeMode = false;
|
|
|
|
CustomizableUI.dispatchToolboxEvent("customizationready", {}, window);
|
|
|
|
if (!this._wantToBeInCustomizeMode) {
|
|
this.exit();
|
|
}
|
|
})().catch(e => {
|
|
lazy.log.error("Error entering customize mode", e);
|
|
this._handler.isEnteringCustomizeMode = false;
|
|
// Exit customize mode to ensure proper clean-up when entering failed.
|
|
this.exit();
|
|
});
|
|
},
|
|
|
|
exit() {
|
|
this._wantToBeInCustomizeMode = false;
|
|
|
|
if (!this._customizing || this._handler.isExitingCustomizeMode) {
|
|
return;
|
|
}
|
|
|
|
// Entering; want to exit once we've done that.
|
|
if (this._handler.isEnteringCustomizeMode) {
|
|
lazy.log.debug(
|
|
"Attempted to exit while we're in the middle of entering. " +
|
|
"We'll exit after we've entered"
|
|
);
|
|
return;
|
|
}
|
|
|
|
if (this.resetting) {
|
|
lazy.log.debug(
|
|
"Attempted to exit while we're resetting. " +
|
|
"We'll exit after resetting has finished."
|
|
);
|
|
return;
|
|
}
|
|
|
|
this._handler.isExitingCustomizeMode = true;
|
|
|
|
this._translationObserver.disconnect();
|
|
|
|
this._teardownDownloadAutoHideToggle();
|
|
|
|
lazy.AddonManager.removeAddonListener(this);
|
|
CustomizableUI.removeListener(this);
|
|
|
|
let window = this.window;
|
|
let document = this.document;
|
|
|
|
document.removeEventListener("keypress", this);
|
|
|
|
this.togglePong(false);
|
|
|
|
// Disable the reset and undo reset buttons while transitioning:
|
|
let resetButton = this.$("customization-reset-button");
|
|
let undoResetButton = this.$("customization-undo-reset-button");
|
|
undoResetButton.hidden = resetButton.disabled = true;
|
|
|
|
this._transitioning = true;
|
|
|
|
this._depopulatePalette();
|
|
|
|
// We need to set this._customizing to false and remove the `customizing`
|
|
// attribute before removing the tab or else
|
|
// XULBrowserWindow.onLocationChange might think that we're still in
|
|
// customization mode and need to exit it for a second time.
|
|
this._customizing = false;
|
|
document.documentElement.removeAttribute("customizing");
|
|
|
|
if (this.browser.selectedTab == gTab) {
|
|
closeGlobalTab();
|
|
}
|
|
|
|
let customizer = document.getElementById("customization-container");
|
|
let browser = document.getElementById("browser");
|
|
customizer.hidden = true;
|
|
browser.hidden = false;
|
|
|
|
window.gNavToolbox.removeEventListener("toolbarvisibilitychange", this);
|
|
|
|
this._teardownPaletteDragging();
|
|
|
|
(async () => {
|
|
await this._unwrapToolbarItems();
|
|
|
|
// 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.menuButton.disabled = false;
|
|
let overflowContainer = document.getElementById(
|
|
"widget-overflow-mainView"
|
|
).firstElementChild;
|
|
overflowContainer.appendChild(window.PanelUI.overflowFixedList);
|
|
document.getElementById("nav-bar-overflow-button").disabled = false;
|
|
let panelContextMenu = document.getElementById(kPanelItemContextMenu);
|
|
this._previousPanelContextMenuParent.appendChild(panelContextMenu);
|
|
|
|
let customizableToolbars = document.querySelectorAll(
|
|
"toolbar[customizable=true]:not([autohide=true])"
|
|
);
|
|
for (let toolbar of customizableToolbars) {
|
|
toolbar.removeAttribute("customizing");
|
|
}
|
|
|
|
this._maybeMoveDownloadsButtonToNavBar();
|
|
|
|
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();
|
|
}
|
|
})().catch(e => {
|
|
lazy.log.error("Error exiting customize mode", e);
|
|
this._handler.isExitingCustomizeMode = false;
|
|
});
|
|
},
|
|
|
|
/**
|
|
* The overflow panel in customize mode should have its arrow pointing
|
|
* at the overflow button. In order to do this correctly, we pass the
|
|
* distance between the inside of window and the middle of the button
|
|
* to the customize mode markup in which the arrow and panel are placed.
|
|
*/
|
|
async _updateOverflowPanelArrowOffset() {
|
|
let currentDensity =
|
|
this.document.documentElement.getAttribute("uidensity");
|
|
let offset = await this.window.promiseDocumentFlushed(() => {
|
|
let overflowButton = this.$("nav-bar-overflow-button");
|
|
let buttonRect = overflowButton.getBoundingClientRect();
|
|
let endDistance;
|
|
if (this.window.RTL_UI) {
|
|
endDistance = buttonRect.left;
|
|
} else {
|
|
endDistance = this.window.innerWidth - buttonRect.right;
|
|
}
|
|
return endDistance + buttonRect.width / 2;
|
|
});
|
|
if (
|
|
!this.document ||
|
|
currentDensity != this.document.documentElement.getAttribute("uidensity")
|
|
) {
|
|
return;
|
|
}
|
|
this.$("customization-panelWrapper").style.setProperty(
|
|
"--panel-arrow-offset",
|
|
offset + "px"
|
|
);
|
|
},
|
|
|
|
_getCustomizableChildForNode(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 = CustomizableUI.getCustomizationTarget(areaNode);
|
|
if (customizationTarget && customizationTarget != areaNode) {
|
|
areas.push(customizationTarget.id);
|
|
}
|
|
let overflowTarget =
|
|
areaNode && areaNode.getAttribute("default-overflowtarget");
|
|
if (overflowTarget) {
|
|
areas.push(overflowTarget);
|
|
}
|
|
}
|
|
areas.push(kPaletteId);
|
|
|
|
while (aNode && aNode.parentNode) {
|
|
let parent = aNode.parentNode;
|
|
if (areas.includes(parent.id)) {
|
|
return aNode;
|
|
}
|
|
aNode = parent;
|
|
}
|
|
return null;
|
|
},
|
|
|
|
_promiseWidgetAnimationOut(aNode) {
|
|
if (
|
|
this.window.gReduceMotion ||
|
|
aNode.getAttribute("cui-anchorid") == "nav-bar-overflow-button" ||
|
|
(aNode.tagName != "toolbaritem" && aNode.tagName != "toolbarbutton") ||
|
|
(aNode.id == "downloads-button" && aNode.hidden)
|
|
) {
|
|
return null;
|
|
}
|
|
|
|
let animationNode;
|
|
if (aNode.parentNode && aNode.parentNode.id.startsWith("wrapper-")) {
|
|
animationNode = aNode.parentNode;
|
|
} else {
|
|
animationNode = aNode;
|
|
}
|
|
return new Promise(resolve => {
|
|
function cleanupCustomizationExit() {
|
|
resolveAnimationPromise();
|
|
}
|
|
|
|
function cleanupWidgetAnimationEnd(e) {
|
|
if (
|
|
e.animationName == "widget-animate-out" &&
|
|
e.target.id == animationNode.id
|
|
) {
|
|
resolveAnimationPromise();
|
|
}
|
|
}
|
|
|
|
function resolveAnimationPromise() {
|
|
animationNode.removeEventListener(
|
|
"animationend",
|
|
cleanupWidgetAnimationEnd
|
|
);
|
|
animationNode.removeEventListener(
|
|
"customizationending",
|
|
cleanupCustomizationExit
|
|
);
|
|
resolve(animationNode);
|
|
}
|
|
|
|
// Wait until the next frame before setting the class to ensure
|
|
// we do start the animation.
|
|
this.window.requestAnimationFrame(() => {
|
|
this.window.requestAnimationFrame(() => {
|
|
animationNode.classList.add("animate-out");
|
|
animationNode.ownerGlobal.gNavToolbox.addEventListener(
|
|
"customizationending",
|
|
cleanupCustomizationExit
|
|
);
|
|
animationNode.addEventListener(
|
|
"animationend",
|
|
cleanupWidgetAnimationEnd
|
|
);
|
|
});
|
|
});
|
|
});
|
|
},
|
|
|
|
async addToToolbar(aNode) {
|
|
aNode = this._getCustomizableChildForNode(aNode);
|
|
if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
|
|
aNode = aNode.firstElementChild;
|
|
}
|
|
let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode);
|
|
let animationNode;
|
|
if (widgetAnimationPromise) {
|
|
animationNode = await widgetAnimationPromise;
|
|
}
|
|
|
|
let widgetToAdd = aNode.id;
|
|
if (
|
|
CustomizableUI.isSpecialWidget(widgetToAdd) &&
|
|
aNode.closest("#customization-palette")
|
|
) {
|
|
widgetToAdd = widgetToAdd.match(
|
|
/^customizableui-special-(spring|spacer|separator)/
|
|
)[1];
|
|
}
|
|
|
|
CustomizableUI.addWidgetToArea(widgetToAdd, CustomizableUI.AREA_NAVBAR);
|
|
lazy.BrowserUsageTelemetry.recordWidgetChange(
|
|
widgetToAdd,
|
|
CustomizableUI.AREA_NAVBAR
|
|
);
|
|
if (!this._customizing) {
|
|
CustomizableUI.dispatchToolboxEvent("customizationchange");
|
|
}
|
|
|
|
// If the user explicitly moves this item, turn off autohide.
|
|
if (aNode.id == "downloads-button") {
|
|
Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
|
|
if (this._customizing) {
|
|
this._showDownloadsAutoHidePanel();
|
|
}
|
|
}
|
|
|
|
if (animationNode) {
|
|
animationNode.classList.remove("animate-out");
|
|
}
|
|
},
|
|
|
|
async addToPanel(aNode, aReason) {
|
|
aNode = this._getCustomizableChildForNode(aNode);
|
|
if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
|
|
aNode = aNode.firstElementChild;
|
|
}
|
|
let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode);
|
|
let animationNode;
|
|
if (widgetAnimationPromise) {
|
|
animationNode = await widgetAnimationPromise;
|
|
}
|
|
|
|
let panel = CustomizableUI.AREA_FIXED_OVERFLOW_PANEL;
|
|
CustomizableUI.addWidgetToArea(aNode.id, panel);
|
|
lazy.BrowserUsageTelemetry.recordWidgetChange(aNode.id, panel, aReason);
|
|
if (!this._customizing) {
|
|
CustomizableUI.dispatchToolboxEvent("customizationchange");
|
|
}
|
|
|
|
// If the user explicitly moves this item, turn off autohide.
|
|
if (aNode.id == "downloads-button") {
|
|
Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
|
|
if (this._customizing) {
|
|
this._showDownloadsAutoHidePanel();
|
|
}
|
|
}
|
|
|
|
if (animationNode) {
|
|
animationNode.classList.remove("animate-out");
|
|
}
|
|
if (!this.window.gReduceMotion) {
|
|
let overflowButton = this.$("nav-bar-overflow-button");
|
|
overflowButton.setAttribute("animate", "true");
|
|
overflowButton.addEventListener(
|
|
"animationend",
|
|
function onAnimationEnd(event) {
|
|
if (event.animationName.startsWith("overflow-animation")) {
|
|
this.removeEventListener("animationend", onAnimationEnd);
|
|
this.removeAttribute("animate");
|
|
}
|
|
}
|
|
);
|
|
}
|
|
},
|
|
|
|
async removeFromArea(aNode, aReason) {
|
|
aNode = this._getCustomizableChildForNode(aNode);
|
|
if (aNode.localName == "toolbarpaletteitem" && aNode.firstElementChild) {
|
|
aNode = aNode.firstElementChild;
|
|
}
|
|
let widgetAnimationPromise = this._promiseWidgetAnimationOut(aNode);
|
|
let animationNode;
|
|
if (widgetAnimationPromise) {
|
|
animationNode = await widgetAnimationPromise;
|
|
}
|
|
|
|
CustomizableUI.removeWidgetFromArea(aNode.id);
|
|
lazy.BrowserUsageTelemetry.recordWidgetChange(aNode.id, null, aReason);
|
|
if (!this._customizing) {
|
|
CustomizableUI.dispatchToolboxEvent("customizationchange");
|
|
}
|
|
|
|
// If the user explicitly removes this item, turn off autohide.
|
|
if (aNode.id == "downloads-button") {
|
|
Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
|
|
if (this._customizing) {
|
|
this._showDownloadsAutoHidePanel();
|
|
}
|
|
}
|
|
if (animationNode) {
|
|
animationNode.classList.remove("animate-out");
|
|
}
|
|
},
|
|
|
|
populatePalette() {
|
|
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);
|
|
}
|
|
|
|
let flexSpace = CustomizableUI.createSpecialWidget(
|
|
"spring",
|
|
this.document
|
|
);
|
|
fragment.appendChild(this.wrapToolbarItem(flexSpace, "palette"));
|
|
|
|
this.visiblePalette.appendChild(fragment);
|
|
this._stowedPalette = this.window.gNavToolbox.palette;
|
|
this.window.gNavToolbox.palette = this.visiblePalette;
|
|
|
|
// Now that the palette items are all here, disable all commands.
|
|
// We do this here rather than directly in `enter` because we
|
|
// need to do/undo this when we're called from reset(), too.
|
|
this._updateCommandsDisabledState(true);
|
|
} catch (ex) {
|
|
lazy.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(aWidget, aPlace) {
|
|
let widgetNode = aWidget.forWindow(this.window).node;
|
|
if (!widgetNode) {
|
|
lazy.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() {
|
|
// Quick, undo the command disabling before we depopulate completely:
|
|
this._updateCommandsDisabledState(false);
|
|
|
|
this.visiblePalette.hidden = true;
|
|
let paletteChild = this.visiblePalette.firstElementChild;
|
|
let nextChild;
|
|
while (paletteChild) {
|
|
nextChild = paletteChild.nextElementSibling;
|
|
let itemId = paletteChild.firstElementChild.id;
|
|
if (CustomizableUI.isSpecialWidget(itemId)) {
|
|
this.visiblePalette.removeChild(paletteChild);
|
|
} else {
|
|
// XXXunf Currently this doesn't destroy the (now unused) node in the
|
|
// API provider case. 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.
|
|
let unwrappedPaletteItem = this.unwrapToolbarItem(paletteChild);
|
|
this._stowedPalette.appendChild(unwrappedPaletteItem);
|
|
}
|
|
|
|
paletteChild = nextChild;
|
|
}
|
|
this.visiblePalette.hidden = false;
|
|
this.window.gNavToolbox.palette = this._stowedPalette;
|
|
},
|
|
|
|
_updateCommandsDisabledState(shouldBeDisabled) {
|
|
for (let command of this.document.querySelectorAll("command")) {
|
|
if (!command.id || !this._enabledCommands.has(command.id)) {
|
|
if (shouldBeDisabled) {
|
|
if (command.getAttribute("disabled") != "true") {
|
|
command.setAttribute("disabled", true);
|
|
} else {
|
|
command.setAttribute("wasdisabled", true);
|
|
}
|
|
} else if (command.getAttribute("wasdisabled") != "true") {
|
|
command.removeAttribute("disabled");
|
|
} else {
|
|
command.removeAttribute("wasdisabled");
|
|
}
|
|
}
|
|
}
|
|
},
|
|
|
|
isCustomizableItem(aNode) {
|
|
return (
|
|
aNode.localName == "toolbarbutton" ||
|
|
aNode.localName == "toolbaritem" ||
|
|
aNode.localName == "toolbarseparator" ||
|
|
aNode.localName == "toolbarspring" ||
|
|
aNode.localName == "toolbarspacer"
|
|
);
|
|
},
|
|
|
|
isWrappedToolbarItem(aNode) {
|
|
return aNode.localName == "toolbarpaletteitem";
|
|
},
|
|
|
|
deferredWrapToolbarItem(aNode, aPlace) {
|
|
return new Promise(resolve => {
|
|
dispatchFunction(() => {
|
|
let wrapper = this.wrapToolbarItem(aNode, aPlace);
|
|
resolve(wrapper);
|
|
});
|
|
});
|
|
},
|
|
|
|
wrapToolbarItem(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;
|
|
},
|
|
|
|
/**
|
|
* Helper to set the label, either directly or to set up the translation
|
|
* observer so we can set the label once it's available.
|
|
*/
|
|
_updateWrapperLabel(aNode, aIsUpdate, aWrapper = aNode.parentElement) {
|
|
if (aNode.hasAttribute("label")) {
|
|
aWrapper.setAttribute("title", aNode.getAttribute("label"));
|
|
aWrapper.setAttribute("tooltiptext", aNode.getAttribute("label"));
|
|
} else if (aNode.hasAttribute("title")) {
|
|
aWrapper.setAttribute("title", aNode.getAttribute("title"));
|
|
aWrapper.setAttribute("tooltiptext", aNode.getAttribute("title"));
|
|
} else if (aNode.hasAttribute("data-l10n-id") && !aIsUpdate) {
|
|
this._translationObserver.observe(aNode, {
|
|
attributes: true,
|
|
attributeFilter: ["label", "title"],
|
|
});
|
|
}
|
|
},
|
|
|
|
/**
|
|
* Called when a node without a label or title is updated.
|
|
*/
|
|
_onTranslations(aMutations) {
|
|
for (let mut of aMutations) {
|
|
let { target } = mut;
|
|
if (
|
|
target.parentElement?.localName == "toolbarpaletteitem" &&
|
|
(target.hasAttribute("label") || mut.target.hasAttribute("title"))
|
|
) {
|
|
this._updateWrapperLabel(target, true);
|
|
}
|
|
}
|
|
},
|
|
|
|
createOrUpdateWrapper(aNode, aPlace, aIsUpdate) {
|
|
let wrapper;
|
|
if (
|
|
aIsUpdate &&
|
|
aNode.parentNode &&
|
|
aNode.parentNode.localName == "toolbarpaletteitem"
|
|
) {
|
|
wrapper = aNode.parentNode;
|
|
aPlace = wrapper.getAttribute("place");
|
|
} else {
|
|
wrapper = this.document.createXULElement("toolbarpaletteitem");
|
|
// "place" is used to show the label 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.
|
|
// Note that some buttons opt out of this with the
|
|
// keepbroadcastattributeswhencustomizing attribute.
|
|
if (
|
|
aNode.hasAttribute("command") &&
|
|
aNode.getAttribute(kKeepBroadcastAttributes) != "true"
|
|
) {
|
|
wrapper.setAttribute("itemcommand", aNode.getAttribute("command"));
|
|
aNode.removeAttribute("command");
|
|
}
|
|
|
|
if (
|
|
aNode.hasAttribute("observes") &&
|
|
aNode.getAttribute(kKeepBroadcastAttributes) != "true"
|
|
) {
|
|
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"));
|
|
}
|
|
|
|
this._updateWrapperLabel(aNode, aIsUpdate, wrapper);
|
|
|
|
if (aNode.hasAttribute("flex")) {
|
|
wrapper.setAttribute("flex", aNode.getAttribute("flex"));
|
|
}
|
|
|
|
let removable =
|
|
aPlace == "palette" || CustomizableUI.isWidgetRemovable(aNode);
|
|
wrapper.setAttribute("removable", removable);
|
|
|
|
// Allow touch events to initiate dragging in customize mode.
|
|
// This is only supported on Windows for now.
|
|
wrapper.setAttribute("touchdownstartsdrag", "true");
|
|
|
|
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);
|
|
}
|
|
|
|
if (CustomizableUI.isSpecialWidget(aNode.id)) {
|
|
wrapper.setAttribute(
|
|
"title",
|
|
lazy.gWidgetsBundle.GetStringFromName(aNode.nodeName + ".label")
|
|
);
|
|
}
|
|
|
|
return wrapper;
|
|
},
|
|
|
|
deferredUnwrapToolbarItem(aWrapper) {
|
|
return new Promise(resolve => {
|
|
dispatchFunction(() => {
|
|
let item = null;
|
|
try {
|
|
item = this.unwrapToolbarItem(aWrapper);
|
|
} catch (ex) {
|
|
console.error(ex);
|
|
}
|
|
resolve(item);
|
|
});
|
|
});
|
|
},
|
|
|
|
unwrapToolbarItem(aWrapper) {
|
|
if (aWrapper.nodeName != "toolbarpaletteitem") {
|
|
return aWrapper;
|
|
}
|
|
aWrapper.removeEventListener("mousedown", this);
|
|
aWrapper.removeEventListener("mouseup", this);
|
|
|
|
let place = aWrapper.getAttribute("place");
|
|
|
|
let toolbarItem = aWrapper.firstElementChild;
|
|
if (!toolbarItem) {
|
|
lazy.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.$(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;
|
|
},
|
|
|
|
async _wrapToolbarItem(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)) {
|
|
await this.deferredWrapToolbarItem(
|
|
child,
|
|
CustomizableUI.getPlaceForItem(child)
|
|
).catch(lazy.log.error);
|
|
}
|
|
}
|
|
this.areas.add(target);
|
|
return target;
|
|
},
|
|
|
|
_wrapToolbarItemSync(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) {
|
|
lazy.log.error(ex, ex.stack);
|
|
}
|
|
|
|
this.areas.add(target);
|
|
return target;
|
|
},
|
|
|
|
async _wrapToolbarItems() {
|
|
for (let area of CustomizableUI.areas) {
|
|
await this._wrapToolbarItem(area);
|
|
}
|
|
},
|
|
|
|
_addDragHandlers(aTarget) {
|
|
// Allow dropping on the padding of the arrow panel.
|
|
if (aTarget.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) {
|
|
aTarget = this.$("customization-panelHolder");
|
|
}
|
|
aTarget.addEventListener("dragstart", this, true);
|
|
aTarget.addEventListener("dragover", this, true);
|
|
aTarget.addEventListener("dragleave", this, true);
|
|
aTarget.addEventListener("drop", this, true);
|
|
aTarget.addEventListener("dragend", this, true);
|
|
},
|
|
|
|
_wrapItemsInArea(target) {
|
|
for (let child of target.children) {
|
|
if (this.isCustomizableItem(child)) {
|
|
this.wrapToolbarItem(child, CustomizableUI.getPlaceForItem(child));
|
|
}
|
|
}
|
|
},
|
|
|
|
_removeDragHandlers(aTarget) {
|
|
// Remove handler from different target if it was added to
|
|
// allow dropping on the padding of the arrow panel.
|
|
if (aTarget.id == CustomizableUI.AREA_FIXED_OVERFLOW_PANEL) {
|
|
aTarget = this.$("customization-panelHolder");
|
|
}
|
|
aTarget.removeEventListener("dragstart", this, true);
|
|
aTarget.removeEventListener("dragover", this, true);
|
|
aTarget.removeEventListener("dragleave", this, true);
|
|
aTarget.removeEventListener("drop", this, true);
|
|
aTarget.removeEventListener("dragend", this, true);
|
|
},
|
|
|
|
_unwrapItemsInArea(target) {
|
|
for (let toolbarItem of target.children) {
|
|
if (this.isWrappedToolbarItem(toolbarItem)) {
|
|
this.unwrapToolbarItem(toolbarItem);
|
|
}
|
|
}
|
|
},
|
|
|
|
_unwrapToolbarItems() {
|
|
return (async () => {
|
|
for (let target of this.areas) {
|
|
for (let toolbarItem of target.children) {
|
|
if (this.isWrappedToolbarItem(toolbarItem)) {
|
|
await this.deferredUnwrapToolbarItem(toolbarItem);
|
|
}
|
|
}
|
|
this._removeDragHandlers(target);
|
|
}
|
|
this.areas.clear();
|
|
})().catch(lazy.log.error);
|
|
},
|
|
|
|
reset() {
|
|
this.resetting = true;
|
|
// Disable the reset button temporarily while resetting:
|
|
let btn = this.$("customization-reset-button");
|
|
btn.disabled = true;
|
|
return (async () => {
|
|
this._depopulatePalette();
|
|
await this._unwrapToolbarItems();
|
|
|
|
CustomizableUI.reset();
|
|
|
|
await this._wrapToolbarItems();
|
|
this.populatePalette();
|
|
|
|
this._updateResetButton();
|
|
this._updateUndoResetButton();
|
|
this._updateEmptyPaletteNotice();
|
|
this._moveDownloadsButtonToNavBar = false;
|
|
this.resetting = false;
|
|
if (!this._wantToBeInCustomizeMode) {
|
|
this.exit();
|
|
}
|
|
})().catch(lazy.log.error);
|
|
},
|
|
|
|
undoReset() {
|
|
this.resetting = true;
|
|
|
|
return (async () => {
|
|
this._depopulatePalette();
|
|
await this._unwrapToolbarItems();
|
|
|
|
CustomizableUI.undoReset();
|
|
|
|
await this._wrapToolbarItems();
|
|
this.populatePalette();
|
|
|
|
this._updateResetButton();
|
|
this._updateUndoResetButton();
|
|
this._updateEmptyPaletteNotice();
|
|
this._moveDownloadsButtonToNavBar = false;
|
|
this.resetting = false;
|
|
})().catch(lazy.log.error);
|
|
},
|
|
|
|
_onToolbarVisibilityChange(aEvent) {
|
|
let toolbar = aEvent.target;
|
|
toolbar.toggleAttribute(
|
|
"customizing",
|
|
aEvent.detail.visible && toolbar.getAttribute("customizable") == "true"
|
|
);
|
|
this._onUIChange();
|
|
},
|
|
|
|
onWidgetMoved() {
|
|
this._onUIChange();
|
|
},
|
|
|
|
onWidgetAdded() {
|
|
this._onUIChange();
|
|
},
|
|
|
|
onWidgetRemoved() {
|
|
this._onUIChange();
|
|
},
|
|
|
|
onWidgetBeforeDOMChange(aNodeToChange, aSecondaryNode, aContainer) {
|
|
if (aContainer.ownerGlobal != this.window || this.resetting) {
|
|
return;
|
|
}
|
|
// 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(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);
|
|
}
|
|
}
|
|
},
|
|
|
|
onWidgetDestroyed(aWidgetId) {
|
|
let wrapper = this.$("wrapper-" + aWidgetId);
|
|
if (wrapper) {
|
|
wrapper.remove();
|
|
}
|
|
},
|
|
|
|
onWidgetAfterCreation(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.$(aWidgetId);
|
|
if (widgetNode) {
|
|
this.wrapToolbarItem(widgetNode, "palette");
|
|
} else {
|
|
let widget = CustomizableUI.getWidget(aWidgetId);
|
|
this.visiblePalette.appendChild(
|
|
this.makePaletteItem(widget, "palette")
|
|
);
|
|
}
|
|
}
|
|
},
|
|
|
|
onAreaNodeRegistered(aArea, aContainer) {
|
|
if (aContainer.ownerDocument == this.document) {
|
|
this._wrapItemsInArea(aContainer);
|
|
this._addDragHandlers(aContainer);
|
|
this.areas.add(aContainer);
|
|
}
|
|
},
|
|
|
|
onAreaNodeUnregistered(aArea, aContainer, aReason) {
|
|
if (
|
|
aContainer.ownerDocument == this.document &&
|
|
aReason == CustomizableUI.REASON_AREA_UNREGISTERED
|
|
) {
|
|
this._unwrapItemsInArea(aContainer);
|
|
this._removeDragHandlers(aContainer);
|
|
this.areas.delete(aContainer);
|
|
}
|
|
},
|
|
|
|
openAddonsManagerThemes() {
|
|
this.window.BrowserAddonUI.openAddonsMgr("addons://list/theme");
|
|
},
|
|
|
|
getMoreThemes(aEvent) {
|
|
aEvent.target.parentNode.parentNode.hidePopup();
|
|
let getMoreURL = Services.urlFormatter.formatURLPref(
|
|
"lightweightThemes.getMoreURL"
|
|
);
|
|
this.window.openTrustedLinkIn(getMoreURL, "tab");
|
|
},
|
|
|
|
updateUIDensity(mode) {
|
|
this.window.gUIDensity.update(mode);
|
|
this._updateOverflowPanelArrowOffset();
|
|
},
|
|
|
|
setUIDensity(mode) {
|
|
let win = this.window;
|
|
let gUIDensity = win.gUIDensity;
|
|
let currentDensity = gUIDensity.getCurrentDensity();
|
|
let panel = win.document.getElementById("customization-uidensity-menu");
|
|
|
|
Services.prefs.setIntPref(gUIDensity.uiDensityPref, mode);
|
|
|
|
// If the user is choosing a different UI density mode while
|
|
// the mode is overriden to Touch, remove the override.
|
|
if (currentDensity.overridden) {
|
|
Services.prefs.setBoolPref(gUIDensity.autoTouchModePref, false);
|
|
}
|
|
|
|
this._onUIChange();
|
|
panel.hidePopup();
|
|
this._updateOverflowPanelArrowOffset();
|
|
},
|
|
|
|
resetUIDensity() {
|
|
this.window.gUIDensity.update();
|
|
this._updateOverflowPanelArrowOffset();
|
|
},
|
|
|
|
onUIDensityMenuShowing() {
|
|
let win = this.window;
|
|
let doc = win.document;
|
|
let gUIDensity = win.gUIDensity;
|
|
let currentDensity = gUIDensity.getCurrentDensity();
|
|
|
|
let normalItem = doc.getElementById(
|
|
"customization-uidensity-menuitem-normal"
|
|
);
|
|
normalItem.mode = gUIDensity.MODE_NORMAL;
|
|
|
|
let items = [normalItem];
|
|
|
|
let compactItem = doc.getElementById(
|
|
"customization-uidensity-menuitem-compact"
|
|
);
|
|
compactItem.mode = gUIDensity.MODE_COMPACT;
|
|
|
|
if (Services.prefs.getBoolPref(kCompactModeShowPref)) {
|
|
compactItem.hidden = false;
|
|
items.push(compactItem);
|
|
} else {
|
|
compactItem.hidden = true;
|
|
}
|
|
|
|
let touchItem = doc.getElementById(
|
|
"customization-uidensity-menuitem-touch"
|
|
);
|
|
// Touch mode can not be enabled in OSX right now.
|
|
if (touchItem) {
|
|
touchItem.mode = gUIDensity.MODE_TOUCH;
|
|
items.push(touchItem);
|
|
}
|
|
|
|
// Mark the active mode menuitem.
|
|
for (let item of items) {
|
|
if (item.mode == currentDensity.mode) {
|
|
item.setAttribute("aria-checked", "true");
|
|
item.setAttribute("active", "true");
|
|
} else {
|
|
item.removeAttribute("aria-checked");
|
|
item.removeAttribute("active");
|
|
}
|
|
}
|
|
|
|
// Add menu items for automatically switching to Touch mode in Windows Tablet Mode.
|
|
if (AppConstants.platform == "win") {
|
|
let spacer = doc.getElementById("customization-uidensity-touch-spacer");
|
|
let checkbox = doc.getElementById(
|
|
"customization-uidensity-autotouchmode-checkbox"
|
|
);
|
|
spacer.removeAttribute("hidden");
|
|
checkbox.removeAttribute("hidden");
|
|
|
|
// Show a hint that the UI density was overridden automatically.
|
|
if (currentDensity.overridden) {
|
|
let sb = Services.strings.createBundle(
|
|
"chrome://browser/locale/uiDensity.properties"
|
|
);
|
|
touchItem.setAttribute(
|
|
"acceltext",
|
|
sb.GetStringFromName("uiDensity.menuitem-touch.acceltext")
|
|
);
|
|
} else {
|
|
touchItem.removeAttribute("acceltext");
|
|
}
|
|
|
|
let autoTouchMode = Services.prefs.getBoolPref(
|
|
win.gUIDensity.autoTouchModePref
|
|
);
|
|
if (autoTouchMode) {
|
|
checkbox.setAttribute("checked", "true");
|
|
} else {
|
|
checkbox.removeAttribute("checked");
|
|
}
|
|
}
|
|
},
|
|
|
|
updateAutoTouchMode(checked) {
|
|
Services.prefs.setBoolPref("browser.touchmode.auto", checked);
|
|
// Re-render the menu items since the active mode might have
|
|
// change because of this.
|
|
this.onUIDensityMenuShowing();
|
|
this._onUIChange();
|
|
},
|
|
|
|
_onUIChange() {
|
|
this._changed = true;
|
|
if (!this.resetting) {
|
|
this._updateResetButton();
|
|
this._updateUndoResetButton();
|
|
this._updateEmptyPaletteNotice();
|
|
}
|
|
CustomizableUI.dispatchToolboxEvent("customizationchange");
|
|
},
|
|
|
|
_updateEmptyPaletteNotice() {
|
|
let paletteItems =
|
|
this.visiblePalette.getElementsByTagName("toolbarpaletteitem");
|
|
let whimsyButton = this.$("whimsy-button");
|
|
|
|
if (
|
|
paletteItems.length == 1 &&
|
|
paletteItems[0].id.includes("wrapper-customizableui-special-spring")
|
|
) {
|
|
whimsyButton.hidden = false;
|
|
} else {
|
|
this.togglePong(false);
|
|
whimsyButton.hidden = true;
|
|
}
|
|
},
|
|
|
|
_updateResetButton() {
|
|
let btn = this.$("customization-reset-button");
|
|
btn.disabled = CustomizableUI.inDefaultState;
|
|
},
|
|
|
|
_updateUndoResetButton() {
|
|
let undoResetButton = this.$("customization-undo-reset-button");
|
|
undoResetButton.hidden = !CustomizableUI.canUndoReset;
|
|
},
|
|
|
|
_updateTouchBarButton() {
|
|
if (AppConstants.platform != "macosx") {
|
|
return;
|
|
}
|
|
let touchBarButton = this.$("customization-touchbar-button");
|
|
let touchBarSpacer = this.$("customization-touchbar-spacer");
|
|
|
|
let isTouchBarInitialized = lazy.gTouchBarUpdater.isTouchBarInitialized();
|
|
touchBarButton.hidden = !isTouchBarInitialized;
|
|
touchBarSpacer.hidden = !isTouchBarInitialized;
|
|
},
|
|
|
|
_updateDensityMenu() {
|
|
// If we're entering Customize Mode, and we're using compact mode,
|
|
// then show the button after that.
|
|
let gUIDensity = this.window.gUIDensity;
|
|
if (gUIDensity.getCurrentDensity().mode == gUIDensity.MODE_COMPACT) {
|
|
Services.prefs.setBoolPref(kCompactModeShowPref, true);
|
|
}
|
|
|
|
let button = this.document.getElementById("customization-uidensity-button");
|
|
button.hidden =
|
|
!Services.prefs.getBoolPref(kCompactModeShowPref) &&
|
|
!button.querySelector("#customization-uidensity-menuitem-touch");
|
|
},
|
|
|
|
handleEvent(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 "dragleave":
|
|
this._onDragLeave(aEvent);
|
|
break;
|
|
case "dragend":
|
|
this._onDragEnd(aEvent);
|
|
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;
|
|
}
|
|
},
|
|
|
|
/**
|
|
* We handle dragover/drop on the outer palette separately
|
|
* to avoid overlap with other drag/drop handlers.
|
|
*/
|
|
_setupPaletteDragging() {
|
|
this._addDragHandlers(this.visiblePalette);
|
|
|
|
this.paletteDragHandler = aEvent => {
|
|
let originalTarget = aEvent.originalTarget;
|
|
if (
|
|
this._isUnwantedDragDrop(aEvent) ||
|
|
this.visiblePalette.contains(originalTarget) ||
|
|
this.$("customization-panelHolder").contains(originalTarget)
|
|
) {
|
|
return;
|
|
}
|
|
// We have a dragover/drop on the palette.
|
|
if (aEvent.type == "dragover") {
|
|
this._onDragOver(aEvent, this.visiblePalette);
|
|
} else {
|
|
this._onDragDrop(aEvent, this.visiblePalette);
|
|
}
|
|
};
|
|
let contentContainer = this.$("customization-content-container");
|
|
contentContainer.addEventListener(
|
|
"dragover",
|
|
this.paletteDragHandler,
|
|
true
|
|
);
|
|
contentContainer.addEventListener("drop", this.paletteDragHandler, true);
|
|
},
|
|
|
|
_teardownPaletteDragging() {
|
|
lazy.DragPositionManager.stop();
|
|
this._removeDragHandlers(this.visiblePalette);
|
|
|
|
let contentContainer = this.$("customization-content-container");
|
|
contentContainer.removeEventListener(
|
|
"dragover",
|
|
this.paletteDragHandler,
|
|
true
|
|
);
|
|
contentContainer.removeEventListener("drop", this.paletteDragHandler, true);
|
|
delete this.paletteDragHandler;
|
|
},
|
|
|
|
observe(aSubject, aTopic) {
|
|
switch (aTopic) {
|
|
case "nsPref:changed":
|
|
this._updateResetButton();
|
|
this._updateUndoResetButton();
|
|
if (this._canDrawInTitlebar()) {
|
|
this._updateTitlebarCheckbox();
|
|
}
|
|
break;
|
|
}
|
|
},
|
|
|
|
async onInstalled(addon) {
|
|
await this.onEnabled(addon);
|
|
},
|
|
|
|
async onEnabled(addon) {
|
|
if (addon.type != "theme") {
|
|
return;
|
|
}
|
|
|
|
if (this._nextThemeChangeUserTriggered) {
|
|
this._onUIChange();
|
|
}
|
|
this._nextThemeChangeUserTriggered = false;
|
|
},
|
|
|
|
_canDrawInTitlebar() {
|
|
return this.window.TabsInTitlebar.systemSupported;
|
|
},
|
|
|
|
_ensureCustomizationPanels() {
|
|
let template = this.$("customizationPanel");
|
|
template.replaceWith(template.content);
|
|
|
|
let wrapper = this.$("customModeWrapper");
|
|
wrapper.replaceWith(wrapper.content);
|
|
},
|
|
|
|
_attachEventListeners() {
|
|
let container = this.$("customization-container");
|
|
|
|
container.addEventListener("command", event => {
|
|
switch (event.target.id) {
|
|
case "customization-titlebar-visibility-checkbox":
|
|
// NB: because command fires after click, by the time we've fired, the checkbox binding
|
|
// will already have switched the button's state, so this is correct:
|
|
this.toggleTitlebar(event.target.checked);
|
|
break;
|
|
case "customization-uidensity-menuitem-compact":
|
|
case "customization-uidensity-menuitem-normal":
|
|
case "customization-uidensity-menuitem-touch":
|
|
this.setUIDensity(event.target.mode);
|
|
break;
|
|
case "customization-uidensity-autotouchmode-checkbox":
|
|
this.updateAutoTouchMode(event.target.checked);
|
|
break;
|
|
case "whimsy-button":
|
|
this.togglePong(event.target.checked);
|
|
break;
|
|
case "customization-touchbar-button":
|
|
this.customizeTouchBar();
|
|
break;
|
|
case "customization-undo-reset-button":
|
|
this.undoReset();
|
|
break;
|
|
case "customization-reset-button":
|
|
this.reset();
|
|
break;
|
|
case "customization-done-button":
|
|
this.exit();
|
|
break;
|
|
}
|
|
});
|
|
|
|
container.addEventListener("popupshowing", event => {
|
|
switch (event.target.id) {
|
|
case "customization-toolbar-menu":
|
|
this.window.ToolbarContextMenu.onViewToolbarsPopupShowing(event);
|
|
break;
|
|
case "customization-uidensity-menu":
|
|
this.onUIDensityMenuShowing();
|
|
break;
|
|
}
|
|
});
|
|
|
|
let updateDensity = event => {
|
|
switch (event.target.id) {
|
|
case "customization-uidensity-menuitem-compact":
|
|
case "customization-uidensity-menuitem-normal":
|
|
case "customization-uidensity-menuitem-touch":
|
|
this.updateUIDensity(event.target.mode);
|
|
}
|
|
};
|
|
let densityMenu = this.document.getElementById(
|
|
"customization-uidensity-menu"
|
|
);
|
|
densityMenu.addEventListener("focus", updateDensity);
|
|
densityMenu.addEventListener("mouseover", updateDensity);
|
|
|
|
let resetDensity = event => {
|
|
switch (event.target.id) {
|
|
case "customization-uidensity-menuitem-compact":
|
|
case "customization-uidensity-menuitem-normal":
|
|
case "customization-uidensity-menuitem-touch":
|
|
this.resetUIDensity();
|
|
}
|
|
};
|
|
densityMenu.addEventListener("blur", resetDensity);
|
|
densityMenu.addEventListener("mouseout", resetDensity);
|
|
|
|
this.$("customization-lwtheme-link").addEventListener("click", () => {
|
|
this.openAddonsManagerThemes();
|
|
});
|
|
},
|
|
|
|
_updateTitlebarCheckbox() {
|
|
let drawInTitlebar = Services.appinfo.drawInTitlebar;
|
|
let checkbox = this.$("customization-titlebar-visibility-checkbox");
|
|
// Drawing in the titlebar means 'hiding' the titlebar.
|
|
// We use the attribute rather than a property because if we're not in
|
|
// customize mode the button is hidden and properties don't work.
|
|
if (drawInTitlebar) {
|
|
checkbox.removeAttribute("checked");
|
|
} else {
|
|
checkbox.setAttribute("checked", "true");
|
|
}
|
|
},
|
|
|
|
toggleTitlebar(aShouldShowTitlebar) {
|
|
// Drawing in the titlebar means not showing the titlebar, hence the negation:
|
|
Services.prefs.setIntPref(kDrawInTitlebarPref, !aShouldShowTitlebar);
|
|
},
|
|
|
|
_getBoundsWithoutFlushing(element) {
|
|
return this.window.windowUtils.getBoundsWithoutFlushing(element);
|
|
},
|
|
|
|
_onDragStart(aEvent) {
|
|
__dumpDragData(aEvent);
|
|
let item = aEvent.target;
|
|
while (item && item.localName != "toolbarpaletteitem") {
|
|
if (
|
|
item.localName == "toolbar" ||
|
|
item.id == kPaletteId ||
|
|
item.id == "customization-panelHolder"
|
|
) {
|
|
return;
|
|
}
|
|
item = item.parentNode;
|
|
}
|
|
|
|
let draggedItem = item.firstElementChild;
|
|
let placeForItem = CustomizableUI.getPlaceForItem(item);
|
|
|
|
let dt = aEvent.dataTransfer;
|
|
let documentId = aEvent.target.ownerDocument.documentElement.id;
|
|
|
|
dt.mozSetDataAt(kDragDataTypePrefix + documentId, draggedItem.id, 0);
|
|
dt.effectAllowed = "move";
|
|
|
|
let itemRect = this._getBoundsWithoutFlushing(draggedItem);
|
|
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,
|
|
};
|
|
|
|
let toolbarParent = draggedItem.closest("toolbar");
|
|
if (toolbarParent) {
|
|
let toolbarRect = this._getBoundsWithoutFlushing(toolbarParent);
|
|
toolbarParent.style.minHeight = toolbarRect.height + "px";
|
|
}
|
|
|
|
gDraggingInToolbars = new Set();
|
|
|
|
// Hack needed so that the dragimage will still show the
|
|
// item as it appeared before it was hidden.
|
|
this._initializeDragAfterMove = () => {
|
|
// 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;
|
|
lazy.DragPositionManager.start(this.window);
|
|
let canUsePrevSibling =
|
|
placeForItem == "toolbar" || placeForItem == "panel";
|
|
if (item.nextElementSibling) {
|
|
this._setDragActive(
|
|
item.nextElementSibling,
|
|
"before",
|
|
draggedItem.id,
|
|
placeForItem
|
|
);
|
|
this._dragOverItem = item.nextElementSibling;
|
|
} else if (canUsePrevSibling && item.previousElementSibling) {
|
|
this._setDragActive(
|
|
item.previousElementSibling,
|
|
"after",
|
|
draggedItem.id,
|
|
placeForItem
|
|
);
|
|
this._dragOverItem = item.previousElementSibling;
|
|
}
|
|
let currentArea = this._getCustomizableParent(item);
|
|
currentArea.setAttribute("draggingover", "true");
|
|
}
|
|
this._initializeDragAfterMove = null;
|
|
this.window.clearTimeout(this._dragInitializeTimeout);
|
|
};
|
|
this._dragInitializeTimeout = this.window.setTimeout(
|
|
this._initializeDragAfterMove,
|
|
0
|
|
);
|
|
},
|
|
|
|
_onDragOver(aEvent, aOverrideTarget) {
|
|
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).length) {
|
|
return;
|
|
}
|
|
|
|
let draggedItemId = aEvent.dataTransfer.mozGetDataAt(
|
|
kDragDataTypePrefix + documentId,
|
|
0
|
|
);
|
|
let draggedWrapper = document.getElementById("wrapper-" + draggedItemId);
|
|
let targetArea = this._getCustomizableParent(
|
|
aOverrideTarget || 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 (!CustomizableUI.canWidgetMoveToArea(draggedItemId, targetArea.id)) {
|
|
return;
|
|
}
|
|
|
|
let targetAreaType = CustomizableUI.getPlaceForItem(targetArea);
|
|
let targetNode = this._getDragOverNode(
|
|
aEvent,
|
|
targetArea,
|
|
targetAreaType,
|
|
draggedItemId
|
|
);
|
|
|
|
// We need to determine the place that the widget is being dropped in
|
|
// the target.
|
|
let dragOverItem, dragValue;
|
|
if (targetNode == CustomizableUI.getCustomizationTarget(targetArea)) {
|
|
// We'll assume if the user is dragging directly over the target, that
|
|
// they're attempting to append a child to that target.
|
|
dragOverItem =
|
|
(targetAreaType == "toolbar"
|
|
? this._findVisiblePreviousSiblingNode(targetNode.lastElementChild)
|
|
: targetNode.lastElementChild) || targetNode;
|
|
dragValue = "after";
|
|
} else {
|
|
let targetParent = targetNode.parentNode;
|
|
let position = Array.prototype.indexOf.call(
|
|
targetParent.children,
|
|
targetNode
|
|
);
|
|
if (position == -1) {
|
|
dragOverItem =
|
|
targetAreaType == "toolbar"
|
|
? this._findVisiblePreviousSiblingNode(targetNode.lastElementChild)
|
|
: targetNode.lastElementChild;
|
|
dragValue = "after";
|
|
} else {
|
|
dragOverItem = targetParent.children[position];
|
|
if (targetAreaType == "toolbar") {
|
|
// Check if the aDraggedItem is hovered past the first half of dragOverItem
|
|
let itemRect = this._getBoundsWithoutFlushing(dragOverItem);
|
|
let dropTargetCenter = itemRect.left + itemRect.width / 2;
|
|
let existingDir = dragOverItem.getAttribute("dragover");
|
|
let dirFactor = this.window.RTL_UI ? -1 : 1;
|
|
if (existingDir == "before") {
|
|
dropTargetCenter +=
|
|
((parseInt(dragOverItem.style.borderInlineStartWidth) || 0) / 2) *
|
|
dirFactor;
|
|
} else {
|
|
dropTargetCenter -=
|
|
((parseInt(dragOverItem.style.borderInlineEndWidth) || 0) / 2) *
|
|
dirFactor;
|
|
}
|
|
let before = this.window.RTL_UI
|
|
? aEvent.clientX > dropTargetCenter
|
|
: aEvent.clientX < dropTargetCenter;
|
|
dragValue = before ? "before" : "after";
|
|
} else if (targetAreaType == "panel") {
|
|
let itemRect = this._getBoundsWithoutFlushing(dragOverItem);
|
|
let dropTargetCenter = itemRect.top + itemRect.height / 2;
|
|
let existingDir = dragOverItem.getAttribute("dragover");
|
|
if (existingDir == "before") {
|
|
dropTargetCenter +=
|
|
(parseInt(dragOverItem.style.borderBlockStartWidth) || 0) / 2;
|
|
} else {
|
|
dropTargetCenter -=
|
|
(parseInt(dragOverItem.style.borderBlockEndWidth) || 0) / 2;
|
|
}
|
|
dragValue = aEvent.clientY < dropTargetCenter ? "before" : "after";
|
|
} else {
|
|
dragValue = "before";
|
|
}
|
|
}
|
|
}
|
|
|
|
if (this._dragOverItem && dragOverItem != this._dragOverItem) {
|
|
this._cancelDragActive(this._dragOverItem, dragOverItem);
|
|
}
|
|
|
|
if (
|
|
dragOverItem != this._dragOverItem ||
|
|
dragValue != dragOverItem.getAttribute("dragover")
|
|
) {
|
|
if (dragOverItem != CustomizableUI.getCustomizationTarget(targetArea)) {
|
|
this._setDragActive(
|
|
dragOverItem,
|
|
dragValue,
|
|
draggedItemId,
|
|
targetAreaType
|
|
);
|
|
}
|
|
this._dragOverItem = dragOverItem;
|
|
targetArea.setAttribute("draggingover", "true");
|
|
}
|
|
|
|
aEvent.preventDefault();
|
|
aEvent.stopPropagation();
|
|
},
|
|
|
|
_onDragDrop(aEvent, aOverrideTarget) {
|
|
if (this._isUnwantedDragDrop(aEvent)) {
|
|
return;
|
|
}
|
|
|
|
__dumpDragData(aEvent);
|
|
this._initializeDragAfterMove = null;
|
|
this.window.clearTimeout(this._dragInitializeTimeout);
|
|
|
|
let targetArea = this._getCustomizableParent(
|
|
aOverrideTarget || 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.nextElementSibling) {
|
|
targetNode = targetNode.nextElementSibling;
|
|
} else {
|
|
targetNode = targetArea;
|
|
}
|
|
}
|
|
if (targetNode.tagName == "toolbarpaletteitem") {
|
|
targetNode = targetNode.firstElementChild;
|
|
}
|
|
|
|
this._cancelDragActive(this._dragOverItem, null, true);
|
|
|
|
try {
|
|
this._applyDrop(
|
|
aEvent,
|
|
targetArea,
|
|
originArea,
|
|
draggedItemId,
|
|
targetNode
|
|
);
|
|
} catch (ex) {
|
|
lazy.log.error(ex, ex.stack);
|
|
}
|
|
|
|
// If the user explicitly moves this item, turn off autohide.
|
|
if (draggedItemId == "downloads-button") {
|
|
Services.prefs.setBoolPref(kDownloadAutoHidePref, false);
|
|
this._showDownloadsAutoHidePanel();
|
|
}
|
|
},
|
|
|
|
_applyDrop(aEvent, aTargetArea, aOriginArea, aDraggedItemId, aTargetNode) {
|
|
let document = aEvent.target.ownerDocument;
|
|
let draggedItem = document.getElementById(aDraggedItemId);
|
|
draggedItem.hidden = false;
|
|
draggedItem.removeAttribute("mousedown");
|
|
|
|
let toolbarParent = draggedItem.closest("toolbar");
|
|
if (toolbarParent) {
|
|
toolbarParent.style.removeProperty("min-height");
|
|
}
|
|
|
|
// Do nothing if the target was dropped onto itself (ie, no change in area
|
|
// or position).
|
|
if (draggedItem == aTargetNode) {
|
|
return;
|
|
}
|
|
|
|
if (!CustomizableUI.canWidgetMoveToArea(aDraggedItemId, aTargetArea.id)) {
|
|
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, "drag");
|
|
lazy.BrowserUsageTelemetry.recordWidgetChange(
|
|
aDraggedItemId,
|
|
null,
|
|
"drag"
|
|
);
|
|
// 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);
|
|
}
|
|
this._onDragEnd(aEvent);
|
|
return;
|
|
}
|
|
|
|
// Skipintoolbarset items won't really be moved:
|
|
let areaCustomizationTarget =
|
|
CustomizableUI.getCustomizationTarget(aTargetArea);
|
|
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 == areaCustomizationTarget) {
|
|
areaCustomizationTarget.appendChild(draggedItem);
|
|
} else {
|
|
this.unwrapToolbarItem(aTargetNode.parentNode);
|
|
areaCustomizationTarget.insertBefore(draggedItem, aTargetNode);
|
|
this.wrapToolbarItem(aTargetNode, place);
|
|
}
|
|
this.wrapToolbarItem(draggedItem, place);
|
|
return;
|
|
}
|
|
|
|
// Force creating a new spacer/spring/separator if dragging from the palette
|
|
if (
|
|
CustomizableUI.isSpecialWidget(aDraggedItemId) &&
|
|
aOriginArea.id == kPaletteId
|
|
) {
|
|
aDraggedItemId = aDraggedItemId.match(
|
|
/^customizableui-special-(spring|spacer|separator)/
|
|
)[1];
|
|
}
|
|
|
|
// Is the target the customization area itself? If so, we just add the
|
|
// widget to the end of the area.
|
|
if (aTargetNode == areaCustomizationTarget) {
|
|
CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id);
|
|
lazy.BrowserUsageTelemetry.recordWidgetChange(
|
|
aDraggedItemId,
|
|
aTargetArea.id,
|
|
"drag"
|
|
);
|
|
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.nextElementSibling;
|
|
if (
|
|
itemForPlacement &&
|
|
itemForPlacement.nodeName == "toolbarpaletteitem"
|
|
) {
|
|
itemForPlacement = itemForPlacement.firstElementChild;
|
|
}
|
|
}
|
|
if (itemForPlacement) {
|
|
let targetNodeId =
|
|
itemForPlacement.nodeName == "toolbarpaletteitem"
|
|
? itemForPlacement.firstElementChild &&
|
|
itemForPlacement.firstElementChild.id
|
|
: itemForPlacement.id;
|
|
placement = CustomizableUI.getPlacementOfWidget(targetNodeId);
|
|
}
|
|
if (!placement) {
|
|
lazy.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);
|
|
lazy.BrowserUsageTelemetry.recordWidgetChange(
|
|
aDraggedItemId,
|
|
aTargetArea.id,
|
|
"drag"
|
|
);
|
|
} else {
|
|
CustomizableUI.addWidgetToArea(aDraggedItemId, aTargetArea.id, position);
|
|
lazy.BrowserUsageTelemetry.recordWidgetChange(
|
|
aDraggedItemId,
|
|
aTargetArea.id,
|
|
"drag"
|
|
);
|
|
}
|
|
|
|
this._onDragEnd(aEvent);
|
|
|
|
// 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);
|
|
}
|
|
},
|
|
|
|
_onDragLeave(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 dragleave 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.
|
|
*
|
|
* Note that that means that this function may be called multiple times by a single drag operation.
|
|
*/
|
|
_onDragEnd(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");
|
|
|
|
let toolbarParent = draggedWrapper.closest("toolbar");
|
|
if (toolbarParent) {
|
|
toolbarParent.style.removeProperty("min-height");
|
|
}
|
|
}
|
|
|
|
if (this._dragOverItem) {
|
|
this._cancelDragActive(this._dragOverItem);
|
|
this._dragOverItem = null;
|
|
}
|
|
lazy.DragPositionManager.stop();
|
|
},
|
|
|
|
_isUnwantedDragDrop(aEvent) {
|
|
// The synthesized events for tests generated by synthesizePlainDragAndDrop
|
|
// and 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(aItem, aValue, aDraggedItemId, aAreaType) {
|
|
if (!aItem) {
|
|
return;
|
|
}
|
|
|
|
if (aItem.getAttribute("dragover") != aValue) {
|
|
aItem.setAttribute("dragover", aValue);
|
|
|
|
let window = aItem.ownerGlobal;
|
|
let draggedItem = window.document.getElementById(aDraggedItemId);
|
|
if (aAreaType == "palette") {
|
|
this._setGridDragActive(aItem, draggedItem, aValue);
|
|
} else {
|
|
let targetArea = this._getCustomizableParent(aItem);
|
|
let makeSpaceImmediately = false;
|
|
if (!gDraggingInToolbars.has(targetArea.id)) {
|
|
gDraggingInToolbars.add(targetArea.id);
|
|
let draggedWrapper = this.$("wrapper-" + aDraggedItemId);
|
|
let originArea = this._getCustomizableParent(draggedWrapper);
|
|
makeSpaceImmediately = originArea == targetArea;
|
|
}
|
|
let propertyToMeasure = aAreaType == "toolbar" ? "width" : "height";
|
|
// Calculate width/height of the item when it'd be dropped in this position.
|
|
let borderWidth = this._getDragItemSize(aItem, draggedItem)[
|
|
propertyToMeasure
|
|
];
|
|
let layoutSide = aAreaType == "toolbar" ? "Inline" : "Block";
|
|
let prop, otherProp;
|
|
if (aValue == "before") {
|
|
prop = "border" + layoutSide + "StartWidth";
|
|
otherProp = "border-" + layoutSide.toLowerCase() + "-end-width";
|
|
} else {
|
|
prop = "border" + layoutSide + "EndWidth";
|
|
otherProp = "border-" + layoutSide.toLowerCase() + "-start-width";
|
|
}
|
|
if (makeSpaceImmediately) {
|
|
aItem.setAttribute("notransition", "true");
|
|
}
|
|
aItem.style[prop] = borderWidth + "px";
|
|
aItem.style.removeProperty(otherProp);
|
|
if (makeSpaceImmediately) {
|
|
// Force a layout flush:
|
|
aItem.getBoundingClientRect();
|
|
aItem.removeAttribute("notransition");
|
|
}
|
|
}
|
|
}
|
|
},
|
|
_cancelDragActive(aItem, aNextItem, aNoTransition) {
|
|
let currentArea = this._getCustomizableParent(aItem);
|
|
if (!currentArea) {
|
|
return;
|
|
}
|
|
let nextArea = aNextItem ? this._getCustomizableParent(aNextItem) : null;
|
|
if (currentArea != nextArea) {
|
|
currentArea.removeAttribute("draggingover");
|
|
}
|
|
let areaType = CustomizableUI.getAreaType(currentArea.id);
|
|
if (areaType) {
|
|
if (aNoTransition) {
|
|
aItem.setAttribute("notransition", "true");
|
|
}
|
|
aItem.removeAttribute("dragover");
|
|
// Remove all property values in the case that the end padding
|
|
// had been set.
|
|
aItem.style.removeProperty("border-inline-start-width");
|
|
aItem.style.removeProperty("border-inline-end-width");
|
|
aItem.style.removeProperty("border-block-start-width");
|
|
aItem.style.removeProperty("border-block-end-width");
|
|
if (aNoTransition) {
|
|
// Force a layout flush:
|
|
aItem.getBoundingClientRect();
|
|
aItem.removeAttribute("notransition");
|
|
}
|
|
} else {
|
|
aItem.removeAttribute("dragover");
|
|
if (aNextItem) {
|
|
if (nextArea == currentArea) {
|
|
// No need to do anything if we're still dragging in this area:
|
|
return;
|
|
}
|
|
}
|
|
// Otherwise, clear everything out:
|
|
let positionManager =
|
|
lazy.DragPositionManager.getManagerForArea(currentArea);
|
|
positionManager.clearPlaceholders(currentArea, aNoTransition);
|
|
}
|
|
},
|
|
|
|
_setGridDragActive(aDragOverNode, aDraggedItem) {
|
|
let targetArea = this._getCustomizableParent(aDragOverNode);
|
|
let draggedWrapper = this.$("wrapper-" + aDraggedItem.id);
|
|
let originArea = this._getCustomizableParent(draggedWrapper);
|
|
let positionManager =
|
|
lazy.DragPositionManager.getManagerForArea(targetArea);
|
|
let draggedSize = this._getDragItemSize(aDragOverNode, aDraggedItem);
|
|
positionManager.insertPlaceholder(
|
|
targetArea,
|
|
aDragOverNode,
|
|
draggedSize,
|
|
originArea == targetArea
|
|
);
|
|
},
|
|
|
|
_getDragItemSize(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.nextElementSibling;
|
|
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(aElement) {
|
|
if (aElement) {
|
|
// Deal with drag/drop on the padding of the panel.
|
|
let containingPanelHolder = aElement.closest(
|
|
"#customization-panelHolder"
|
|
);
|
|
if (containingPanelHolder) {
|
|
return containingPanelHolder.querySelector(
|
|
"#widget-overflow-fixed-list"
|
|
);
|
|
}
|
|
}
|
|
|
|
let areas = CustomizableUI.areas;
|
|
areas.push(kPaletteId);
|
|
return aElement.closest(areas.map(a => "#" + CSS.escape(a)).join(","));
|
|
},
|
|
|
|
_getDragOverNode(aEvent, aAreaElement, aAreaType) {
|
|
let expectedParent =
|
|
CustomizableUI.getCustomizationTarget(aAreaElement) || aAreaElement;
|
|
if (!expectedParent.contains(aEvent.target)) {
|
|
return expectedParent;
|
|
}
|
|
// 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;
|
|
let bounds = this._getBoundsWithoutFlushing(boundsContainer);
|
|
dragX = Math.min(bounds.right, Math.max(dragX, bounds.left));
|
|
dragY = Math.min(bounds.bottom, Math.max(dragY, bounds.top));
|
|
|
|
let targetNode;
|
|
if (aAreaType == "toolbar" || aAreaType == "panel") {
|
|
targetNode = aAreaElement.ownerDocument.elementFromPoint(dragX, dragY);
|
|
while (targetNode && targetNode.parentNode != expectedParent) {
|
|
targetNode = targetNode.parentNode;
|
|
}
|
|
} else {
|
|
let positionManager =
|
|
lazy.DragPositionManager.getManagerForArea(aAreaElement);
|
|
// Make it relative to the container:
|
|
dragX -= bounds.left;
|
|
dragY -= bounds.top;
|
|
// Find the closest node:
|
|
targetNode = positionManager.find(aAreaElement, dragX, dragY);
|
|
}
|
|
return targetNode || aEvent.target;
|
|
},
|
|
|
|
_onMouseDown(aEvent) {
|
|
lazy.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.toggleAttribute("mousedown", true);
|
|
}
|
|
},
|
|
|
|
_onMouseUp(aEvent) {
|
|
lazy.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(aElement) {
|
|
while (aElement && aElement.localName != "toolbarpaletteitem") {
|
|
if (aElement.localName == "toolbar") {
|
|
return null;
|
|
}
|
|
aElement = aElement.parentNode;
|
|
}
|
|
return aElement;
|
|
},
|
|
|
|
_findVisiblePreviousSiblingNode(aReferenceNode) {
|
|
while (
|
|
aReferenceNode &&
|
|
aReferenceNode.localName == "toolbarpaletteitem" &&
|
|
aReferenceNode.firstElementChild.hidden
|
|
) {
|
|
aReferenceNode = aReferenceNode.previousElementSibling;
|
|
}
|
|
return aReferenceNode;
|
|
},
|
|
|
|
onPaletteContextMenuShowing(event) {
|
|
let isFlexibleSpace = event.target.triggerNode.id.includes(
|
|
"wrapper-customizableui-special-spring"
|
|
);
|
|
event.target.querySelector(".customize-context-addToPanel").disabled =
|
|
isFlexibleSpace;
|
|
},
|
|
|
|
onPanelContextMenuShowing(event) {
|
|
let inPermanentArea = !!event.target.triggerNode.closest(
|
|
"#widget-overflow-fixed-list"
|
|
);
|
|
let doc = event.target.ownerDocument;
|
|
doc.getElementById("customizationPanelItemContextMenuUnpin").hidden =
|
|
!inPermanentArea;
|
|
doc.getElementById("customizationPanelItemContextMenuPin").hidden =
|
|
inPermanentArea;
|
|
|
|
doc.ownerGlobal.MozXULElement.insertFTLIfNeeded(
|
|
"browser/toolbarContextMenu.ftl"
|
|
);
|
|
event.target.querySelectorAll("[data-lazy-l10n-id]").forEach(el => {
|
|
el.setAttribute("data-l10n-id", el.getAttribute("data-lazy-l10n-id"));
|
|
el.removeAttribute("data-lazy-l10n-id");
|
|
});
|
|
},
|
|
|
|
_checkForDownloadsClick(event) {
|
|
if (
|
|
event.target.closest("#wrapper-downloads-button") &&
|
|
event.button == 0
|
|
) {
|
|
event.view.gCustomizeMode._showDownloadsAutoHidePanel();
|
|
}
|
|
},
|
|
|
|
_setupDownloadAutoHideToggle() {
|
|
this.window.addEventListener("click", this._checkForDownloadsClick, true);
|
|
},
|
|
|
|
_teardownDownloadAutoHideToggle() {
|
|
this.window.removeEventListener(
|
|
"click",
|
|
this._checkForDownloadsClick,
|
|
true
|
|
);
|
|
this.$(kDownloadAutohidePanelId).hidePopup();
|
|
},
|
|
|
|
_maybeMoveDownloadsButtonToNavBar() {
|
|
// If the user toggled the autohide checkbox while the item was in the
|
|
// palette, and hasn't moved it since, move the item to the default
|
|
// location in the navbar for them.
|
|
if (
|
|
!CustomizableUI.getPlacementOfWidget("downloads-button") &&
|
|
this._moveDownloadsButtonToNavBar &&
|
|
this.window.DownloadsButton.autoHideDownloadsButton
|
|
) {
|
|
let navbarPlacements = CustomizableUI.getWidgetIdsInArea("nav-bar");
|
|
let insertionPoint = navbarPlacements.indexOf("urlbar-container");
|
|
while (++insertionPoint < navbarPlacements.length) {
|
|
let widget = navbarPlacements[insertionPoint];
|
|
// If we find a non-searchbar, non-spacer node, break out of the loop:
|
|
if (
|
|
widget != "search-container" &&
|
|
!(CustomizableUI.isSpecialWidget(widget) && widget.includes("spring"))
|
|
) {
|
|
break;
|
|
}
|
|
}
|
|
CustomizableUI.addWidgetToArea(
|
|
"downloads-button",
|
|
"nav-bar",
|
|
insertionPoint
|
|
);
|
|
lazy.BrowserUsageTelemetry.recordWidgetChange(
|
|
"downloads-button",
|
|
"nav-bar",
|
|
"move-downloads"
|
|
);
|
|
}
|
|
},
|
|
|
|
async _showDownloadsAutoHidePanel() {
|
|
let doc = this.document;
|
|
let panel = doc.getElementById(kDownloadAutohidePanelId);
|
|
panel.hidePopup();
|
|
let button = doc.getElementById("downloads-button");
|
|
// We don't show the tooltip if the button is in the panel.
|
|
if (button.closest("#widget-overflow-fixed-list")) {
|
|
return;
|
|
}
|
|
|
|
let offsetX = 0,
|
|
offsetY = 0;
|
|
let panelOnTheLeft = false;
|
|
let toolbarContainer = button.closest("toolbar");
|
|
if (toolbarContainer && toolbarContainer.id == "nav-bar") {
|
|
let navbarWidgets = CustomizableUI.getWidgetIdsInArea("nav-bar");
|
|
if (
|
|
navbarWidgets.indexOf("urlbar-container") <=
|
|
navbarWidgets.indexOf("downloads-button")
|
|
) {
|
|
panelOnTheLeft = true;
|
|
}
|
|
} else {
|
|
await this.window.promiseDocumentFlushed(() => {});
|
|
|
|
if (!this._customizing || !this._wantToBeInCustomizeMode) {
|
|
return;
|
|
}
|
|
let buttonBounds = this._getBoundsWithoutFlushing(button);
|
|
let windowBounds = this._getBoundsWithoutFlushing(doc.documentElement);
|
|
panelOnTheLeft =
|
|
buttonBounds.left + buttonBounds.width / 2 > windowBounds.width / 2;
|
|
}
|
|
let position;
|
|
if (panelOnTheLeft) {
|
|
// Tested in RTL, these get inverted automatically, so this does the
|
|
// right thing without taking RTL into account explicitly.
|
|
position = "topleft topright";
|
|
if (toolbarContainer) {
|
|
offsetX = 8;
|
|
}
|
|
} else {
|
|
position = "topright topleft";
|
|
if (toolbarContainer) {
|
|
offsetX = -8;
|
|
}
|
|
}
|
|
|
|
let checkbox = doc.getElementById(kDownloadAutohideCheckboxId);
|
|
if (this.window.DownloadsButton.autoHideDownloadsButton) {
|
|
checkbox.setAttribute("checked", "true");
|
|
} else {
|
|
checkbox.removeAttribute("checked");
|
|
}
|
|
|
|
// We don't use the icon to anchor because it might be resizing because of
|
|
// the animations for drag/drop. Hence the use of offsets.
|
|
panel.openPopup(button, position, offsetX, offsetY);
|
|
},
|
|
|
|
onDownloadsAutoHideChange(event) {
|
|
let checkbox = event.target.ownerDocument.getElementById(
|
|
kDownloadAutohideCheckboxId
|
|
);
|
|
Services.prefs.setBoolPref(kDownloadAutoHidePref, checkbox.checked);
|
|
// Ensure we move the button (back) after the user leaves customize mode.
|
|
event.view.gCustomizeMode._moveDownloadsButtonToNavBar = checkbox.checked;
|
|
},
|
|
|
|
customizeTouchBar() {
|
|
let updater = Cc["@mozilla.org/widget/touchbarupdater;1"].getService(
|
|
Ci.nsITouchBarUpdater
|
|
);
|
|
updater.enterCustomizeMode();
|
|
},
|
|
|
|
togglePong(enabled) {
|
|
// It's possible we're toggling for a reason other than hitting
|
|
// the button (we might be exiting, for example), so make sure that
|
|
// the state and checkbox are in sync.
|
|
let whimsyButton = this.$("whimsy-button");
|
|
whimsyButton.checked = enabled;
|
|
|
|
if (enabled) {
|
|
this.visiblePalette.setAttribute("whimsypong", "true");
|
|
this.pongArena.hidden = false;
|
|
if (!this.uninitWhimsy) {
|
|
this.uninitWhimsy = this.whimsypong();
|
|
}
|
|
} else {
|
|
this.visiblePalette.removeAttribute("whimsypong");
|
|
if (this.uninitWhimsy) {
|
|
this.uninitWhimsy();
|
|
this.uninitWhimsy = null;
|
|
}
|
|
this.pongArena.hidden = true;
|
|
}
|
|
},
|
|
|
|
whimsypong() {
|
|
function update() {
|
|
updateBall();
|
|
updatePlayers();
|
|
}
|
|
|
|
function updateBall() {
|
|
if (ball[1] <= 0 || ball[1] >= gameSide) {
|
|
if (
|
|
(ball[1] <= 0 && (ball[0] < p1 || ball[0] > p1 + paddleWidth)) ||
|
|
(ball[1] >= gameSide && (ball[0] < p2 || ball[0] > p2 + paddleWidth))
|
|
) {
|
|
updateScore(ball[1] <= 0 ? 0 : 1);
|
|
} else {
|
|
if (
|
|
(ball[1] <= 0 &&
|
|
(ball[0] - p1 < paddleEdge ||
|
|
p1 + paddleWidth - ball[0] < paddleEdge)) ||
|
|
(ball[1] >= gameSide &&
|
|
(ball[0] - p2 < paddleEdge ||
|
|
p2 + paddleWidth - ball[0] < paddleEdge))
|
|
) {
|
|
ballDxDy[0] *= Math.random() + 1.3;
|
|
ballDxDy[0] = Math.max(Math.min(ballDxDy[0], 6), -6);
|
|
if (Math.abs(ballDxDy[0]) == 6) {
|
|
ballDxDy[0] += Math.sign(ballDxDy[0]) * Math.random();
|
|
}
|
|
} else {
|
|
ballDxDy[0] /= 1.1;
|
|
}
|
|
ballDxDy[1] *= -1;
|
|
ball[1] = ball[1] <= 0 ? 0 : gameSide;
|
|
}
|
|
}
|
|
ball = [
|
|
Math.max(Math.min(ball[0] + ballDxDy[0], gameSide), 0),
|
|
Math.max(Math.min(ball[1] + ballDxDy[1], gameSide), 0),
|
|
];
|
|
if (ball[0] <= 0 || ball[0] >= gameSide) {
|
|
ballDxDy[0] *= -1;
|
|
}
|
|
}
|
|
|
|
function updatePlayers() {
|
|
if (keydown) {
|
|
let p1Adj = 1;
|
|
if (
|
|
(keydown == 37 && !window.RTL_UI) ||
|
|
(keydown == 39 && window.RTL_UI)
|
|
) {
|
|
p1Adj = -1;
|
|
}
|
|
p1 += p1Adj * 10 * keydownAdj;
|
|
}
|
|
|
|
let sign = Math.sign(ballDxDy[0]);
|
|
if (
|
|
(sign > 0 && ball[0] > p2 + paddleWidth / 2) ||
|
|
(sign < 0 && ball[0] < p2 + paddleWidth / 2)
|
|
) {
|
|
p2 += sign * 3;
|
|
} else if (
|
|
(sign > 0 && ball[0] > p2 + paddleWidth / 1.1) ||
|
|
(sign < 0 && ball[0] < p2 + paddleWidth / 1.1)
|
|
) {
|
|
p2 += sign * 9;
|
|
}
|
|
|
|
if (score >= winScore) {
|
|
p1 = ball[0];
|
|
p2 = ball[0];
|
|
}
|
|
p1 = Math.max(Math.min(p1, gameSide - paddleWidth), 0);
|
|
p2 = Math.max(Math.min(p2, gameSide - paddleWidth), 0);
|
|
}
|
|
|
|
function updateScore(adj) {
|
|
if (adj) {
|
|
score += adj;
|
|
} else if (--lives == 0) {
|
|
quit = true;
|
|
}
|
|
ball = ballDef.slice();
|
|
ballDxDy = ballDxDyDef.slice();
|
|
ballDxDy[1] *= score / winScore + 1;
|
|
}
|
|
|
|
function draw() {
|
|
let xAdj = window.RTL_UI ? -1 : 1;
|
|
elements["wp-player1"].style.transform =
|
|
"translate(" + xAdj * p1 + "px, -37px)";
|
|
elements["wp-player2"].style.transform =
|
|
"translate(" + xAdj * p2 + "px, " + gameSide + "px)";
|
|
elements["wp-ball"].style.transform =
|
|
"translate(" + xAdj * ball[0] + "px, " + ball[1] + "px)";
|
|
elements["wp-score"].textContent = score;
|
|
elements["wp-lives"].setAttribute("lives", lives);
|
|
if (score >= winScore) {
|
|
let arena = elements.arena;
|
|
let image = "url(chrome://browser/skin/customizableui/whimsy.png)";
|
|
let position = `${
|
|
(window.RTL_UI ? gameSide : 0) + xAdj * ball[0] - 10
|
|
}px ${ball[1] - 10}px`;
|
|
let repeat = "no-repeat";
|
|
let size = "20px";
|
|
if (arena.style.backgroundImage) {
|
|
if (arena.style.backgroundImage.split(",").length >= 160) {
|
|
quit = true;
|
|
}
|
|
|
|
image += ", " + arena.style.backgroundImage;
|
|
position += ", " + arena.style.backgroundPosition;
|
|
repeat += ", " + arena.style.backgroundRepeat;
|
|
size += ", " + arena.style.backgroundSize;
|
|
}
|
|
arena.style.backgroundImage = image;
|
|
arena.style.backgroundPosition = position;
|
|
arena.style.backgroundRepeat = repeat;
|
|
arena.style.backgroundSize = size;
|
|
}
|
|
}
|
|
|
|
function onkeydown(event) {
|
|
keys.push(event.which);
|
|
if (keys.length > 10) {
|
|
keys.shift();
|
|
let codeEntered = true;
|
|
for (let i = 0; i < keys.length; i++) {
|
|
if (keys[i] != keysCode[i]) {
|
|
codeEntered = false;
|
|
break;
|
|
}
|
|
}
|
|
if (codeEntered) {
|
|
elements.arena.setAttribute("kcode", "true");
|
|
let spacer = document.querySelector(
|
|
"#customization-palette > toolbarpaletteitem"
|
|
);
|
|
spacer.setAttribute("kcode", "true");
|
|
}
|
|
}
|
|
if (event.which == 37 /* left */ || event.which == 39 /* right */) {
|
|
keydown = event.which;
|
|
keydownAdj *= 1.05;
|
|
}
|
|
}
|
|
|
|
function onkeyup(event) {
|
|
if (event.which == 37 || event.which == 39) {
|
|
keydownAdj = 1;
|
|
keydown = 0;
|
|
}
|
|
}
|
|
|
|
function uninit() {
|
|
document.removeEventListener("keydown", onkeydown);
|
|
document.removeEventListener("keyup", onkeyup);
|
|
if (rAFHandle) {
|
|
window.cancelAnimationFrame(rAFHandle);
|
|
}
|
|
let arena = elements.arena;
|
|
while (arena.firstChild) {
|
|
arena.firstChild.remove();
|
|
}
|
|
arena.removeAttribute("score");
|
|
arena.removeAttribute("lives");
|
|
arena.removeAttribute("kcode");
|
|
arena.style.removeProperty("background-image");
|
|
arena.style.removeProperty("background-position");
|
|
arena.style.removeProperty("background-repeat");
|
|
arena.style.removeProperty("background-size");
|
|
let spacer = document.querySelector(
|
|
"#customization-palette > toolbarpaletteitem"
|
|
);
|
|
spacer.removeAttribute("kcode");
|
|
elements = null;
|
|
document = null;
|
|
quit = true;
|
|
}
|
|
|
|
if (this.uninitWhimsy) {
|
|
return this.uninitWhimsy;
|
|
}
|
|
|
|
let ballDef = [10, 10];
|
|
let ball = [10, 10];
|
|
let ballDxDyDef = [2, 2];
|
|
let ballDxDy = [2, 2];
|
|
let score = 0;
|
|
let p1 = 0;
|
|
let p2 = 10;
|
|
let gameSide = 300;
|
|
let paddleEdge = 30;
|
|
let paddleWidth = 84;
|
|
let keydownAdj = 1;
|
|
let keydown = 0;
|
|
let keys = [];
|
|
let keysCode = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65];
|
|
let lives = 5;
|
|
let winScore = 11;
|
|
let quit = false;
|
|
let document = this.document;
|
|
let rAFHandle = 0;
|
|
let elements = {
|
|
arena: document.getElementById("customization-pong-arena"),
|
|
};
|
|
|
|
document.addEventListener("keydown", onkeydown);
|
|
document.addEventListener("keyup", onkeyup);
|
|
|
|
for (let id of ["player1", "player2", "ball", "score", "lives"]) {
|
|
let el = document.createXULElement("box");
|
|
el.id = "wp-" + id;
|
|
elements[el.id] = elements.arena.appendChild(el);
|
|
}
|
|
|
|
let spacer = this.visiblePalette.querySelector("toolbarpaletteitem");
|
|
for (let player of ["#wp-player1", "#wp-player2"]) {
|
|
let val = "-moz-element(#" + spacer.id + ") no-repeat";
|
|
elements.arena.querySelector(player).style.background = val;
|
|
}
|
|
|
|
let window = this.window;
|
|
rAFHandle = window.requestAnimationFrame(function animate() {
|
|
update();
|
|
draw();
|
|
if (quit) {
|
|
elements["wp-score"].textContent = score;
|
|
elements["wp-lives"] &&
|
|
elements["wp-lives"].setAttribute("lives", lives);
|
|
elements.arena.setAttribute("score", score);
|
|
elements.arena.setAttribute("lives", lives);
|
|
} else {
|
|
rAFHandle = window.requestAnimationFrame(animate);
|
|
}
|
|
});
|
|
|
|
return uninit;
|
|
},
|
|
};
|
|
|
|
function __dumpDragData(aEvent, caller) {
|
|
if (!gDebug) {
|
|
return;
|
|
}
|
|
let str =
|
|
"Dumping drag data (" +
|
|
(caller ? caller + " in " : "") +
|
|
"CustomizeMode.sys.mjs) {\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 += "}";
|
|
lazy.log.debug(str);
|
|
}
|
|
|
|
function dispatchFunction(aFunc) {
|
|
Services.tm.dispatchToMainThread(aFunc);
|
|
}
|