gecko-dev/browser/components/customizableui/content/toolbar.xml

619 строки
23 KiB
XML

<?xml version="1.0"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<bindings id="browserToolbarBindings"
xmlns="http://www.mozilla.org/xbl"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:xbl="http://www.mozilla.org/xbl">
<binding id="toolbar" role="xul:toolbar">
<resources>
<stylesheet src="chrome://global/skin/toolbar.css"/>
</resources>
<implementation>
<field name="overflowedDuringConstruction">null</field>
<constructor><![CDATA[
let scope = {};
Cu.import("resource:///modules/CustomizableUI.jsm", scope);
// Add an early overflow event listener that will mark if the
// toolbar overflowed during construction.
if (scope.CustomizableUI.isAreaOverflowable(this.id)) {
this.addEventListener("overflow", this);
this.addEventListener("underflow", this);
}
if (document.readyState == "complete") {
this._init();
} else {
// Need to wait until XUL overlays are loaded. See bug 554279.
let self = this;
document.addEventListener("readystatechange", function onReadyStateChange() {
if (document.readyState != "complete")
return;
document.removeEventListener("readystatechange", onReadyStateChange);
self._init();
});
}
]]></constructor>
<method name="_init">
<body><![CDATA[
let scope = {};
Cu.import("resource:///modules/CustomizableUI.jsm", scope);
let CustomizableUI = scope.CustomizableUI;
// Bug 989289: Forcibly set the now unsupported "mode" and "iconsize"
// attributes, just in case they accidentally get restored from
// persistence from a user that's been upgrading and downgrading.
if (CustomizableUI.isBuiltinToolbar(this.id)) {
const kAttributes = new Map([["mode", "icons"], ["iconsize", "small"]]);
for (let [attribute, value] of kAttributes) {
if (this.getAttribute(attribute) != value) {
this.setAttribute(attribute, value);
document.persist(this.id, attribute);
}
if (this.toolbox) {
if (this.toolbox.getAttribute(attribute) != value) {
this.toolbox.setAttribute(attribute, value);
document.persist(this.toolbox.id, attribute);
}
}
}
}
// Searching for the toolbox palette in the toolbar binding because
// toolbars are constructed first.
let toolbox = this.toolbox;
if (toolbox && !toolbox.palette) {
for (let node of toolbox.children) {
if (node.localName == "toolbarpalette") {
// Hold on to the palette but remove it from the document.
toolbox.palette = node;
toolbox.removeChild(node);
break;
}
}
}
// pass the current set of children for comparison with placements:
let children = Array.from(this.childNodes)
.filter(node => node.getAttribute("skipintoolbarset") != "true" && node.id)
.map(node => node.id);
CustomizableUI.registerToolbarNode(this, children);
]]></body>
</method>
<method name="handleEvent">
<parameter name="aEvent"/>
<body><![CDATA[
if (aEvent.type == "overflow" && aEvent.detail > 0) {
if (this.overflowable && this.overflowable.initialized) {
this.overflowable.onOverflow(aEvent);
} else {
this.overflowedDuringConstruction = aEvent;
}
} else if (aEvent.type == "underflow" && aEvent.detail > 0) {
this.overflowedDuringConstruction = null;
}
]]></body>
</method>
<method name="insertItem">
<parameter name="aId"/>
<parameter name="aBeforeElt"/>
<parameter name="aWrapper"/>
<body><![CDATA[
if (aWrapper) {
Cu.reportError("Can't insert " + aId + ": using insertItem " +
"no longer supports wrapper elements.");
return null;
}
// Hack, the customizable UI code makes this be the last position
let pos = null;
if (aBeforeElt) {
let beforeInfo = CustomizableUI.getPlacementOfWidget(aBeforeElt.id);
if (beforeInfo.area != this.id) {
Cu.reportError("Can't insert " + aId + " before " +
aBeforeElt.id + " which isn't in this area (" +
this.id + ").");
return null;
}
pos = beforeInfo.position;
}
CustomizableUI.addWidgetToArea(aId, this.id, pos);
return this.ownerDocument.getElementById(aId);
]]></body>
</method>
<property name="toolbarName"
onget="return this.getAttribute('toolbarname');"
onset="this.setAttribute('toolbarname', val); return val;"/>
<property name="customizationTarget" readonly="true">
<getter><![CDATA[
if (this._customizationTarget)
return this._customizationTarget;
let id = this.getAttribute("customizationtarget");
if (id)
this._customizationTarget = document.getElementById(id);
if (this._customizationTarget)
this._customizationTarget.insertItem = this.insertItem.bind(this);
else
this._customizationTarget = this;
return this._customizationTarget;
]]></getter>
</property>
<property name="toolbox" readonly="true">
<getter><![CDATA[
if (this._toolbox)
return this._toolbox;
let toolboxId = this.getAttribute("toolboxid");
if (toolboxId) {
let toolbox = document.getElementById(toolboxId);
if (toolbox) {
if (toolbox.externalToolbars.indexOf(this) == -1)
toolbox.externalToolbars.push(this);
this._toolbox = toolbox;
}
}
if (!this._toolbox && this.parentNode &&
this.parentNode.localName == "toolbox") {
this._toolbox = this.parentNode;
}
return this._toolbox;
]]></getter>
</property>
<property name="currentSet">
<getter><![CDATA[
let currentWidgets = new Set();
for (let node of this.customizationTarget.children) {
let realNode = node.localName == "toolbarpaletteitem" ? node.firstChild : node;
if (realNode.getAttribute("skipintoolbarset") != "true") {
currentWidgets.add(realNode.id);
}
}
if (this.getAttribute("overflowing") == "true") {
let overflowTarget = this.getAttribute("overflowtarget");
let overflowList = this.ownerDocument.getElementById(overflowTarget);
for (let node of overflowList.children) {
let realNode = node.localName == "toolbarpaletteitem" ? node.firstChild : node;
if (realNode.getAttribute("skipintoolbarset") != "true") {
currentWidgets.add(realNode.id);
}
}
}
let orderedPlacements = CustomizableUI.getWidgetIdsInArea(this.id);
return orderedPlacements.filter((x) => currentWidgets.has(x)).join(",");
]]></getter>
<setter><![CDATA[
// Get list of new and old ids:
let newVal = (val || "").split(",").filter(x => x);
let oldIds = CustomizableUI.getWidgetIdsInArea(this.id);
// Get a list of items only in the new list
let newIds = newVal.filter(id => oldIds.indexOf(id) == -1);
CustomizableUI.beginBatchUpdate();
try {
for (let newId of newIds) {
oldIds = CustomizableUI.getWidgetIdsInArea(this.id);
let nextId = newId;
let pos;
do {
// Get the next item
nextId = newVal[newVal.indexOf(nextId) + 1];
// Figure out where it is in the old list
pos = oldIds.indexOf(nextId);
// If it's not in the old list, repeat:
} while (pos == -1 && nextId);
if (pos == -1) {
pos = null; // We didn't find anything, insert at the end
}
CustomizableUI.addWidgetToArea(newId, this.id, pos);
}
let currentIds = this.currentSet.split(",");
let removedIds = currentIds.filter(id => newIds.indexOf(id) == -1 && newVal.indexOf(id) == -1);
for (let removedId of removedIds) {
CustomizableUI.removeWidgetFromArea(removedId);
}
} finally {
CustomizableUI.endBatchUpdate();
}
]]></setter>
</property>
</implementation>
</binding>
<binding id="toolbar-menubar-stub">
<implementation>
<property name="toolbox" readonly="true">
<getter><![CDATA[
if (this._toolbox)
return this._toolbox;
if (this.parentNode && this.parentNode.localName == "toolbox") {
this._toolbox = this.parentNode;
}
return this._toolbox;
]]></getter>
</property>
<property name="currentSet" readonly="true">
<getter><![CDATA[
return this.getAttribute("defaultset");
]]></getter>
</property>
<method name="insertItem">
<body><![CDATA[
return null;
]]></body>
</method>
</implementation>
</binding>
<!-- The toolbar-menubar-autohide and toolbar-drag bindings are almost
verbatim copies of their toolkit counterparts - they just inherit from
the customizableui's toolbar binding instead of toolkit's. We're currently
OK with the maintainance burden of having two copies of a binding, since
the long term goal is to move the customization framework into toolkit. -->
<binding id="toolbar-menubar-autohide"
extends="chrome://browser/content/customizableui/toolbar.xml#toolbar">
<implementation>
<constructor>
this._setInactive();
</constructor>
<destructor>
this._setActive();
</destructor>
<field name="_inactiveTimeout">null</field>
<field name="_contextMenuListener"><![CDATA[({
toolbar: this,
contextMenu: null,
get active() {
return !!this.contextMenu;
},
init(event) {
let node = event.target;
while (node != this.toolbar) {
if (node.localName == "menupopup")
return;
node = node.parentNode;
}
let contextMenuId = this.toolbar.getAttribute("context");
if (!contextMenuId)
return;
this.contextMenu = document.getElementById(contextMenuId);
if (!this.contextMenu)
return;
this.contextMenu.addEventListener("popupshown", this);
this.contextMenu.addEventListener("popuphiding", this);
this.toolbar.addEventListener("mousemove", this);
},
handleEvent(event) {
switch (event.type) {
case "popupshown":
this.toolbar.removeEventListener("mousemove", this);
break;
case "popuphiding":
case "mousemove":
this.toolbar._setInactiveAsync();
this.toolbar.removeEventListener("mousemove", this);
this.contextMenu.removeEventListener("popuphiding", this);
this.contextMenu.removeEventListener("popupshown", this);
this.contextMenu = null;
break;
}
}
})]]></field>
<method name="_setInactive">
<body><![CDATA[
this.setAttribute("inactive", "true");
]]></body>
</method>
<method name="_setInactiveAsync">
<body><![CDATA[
this._inactiveTimeout = setTimeout(function(self) {
if (self.getAttribute("autohide") == "true") {
self._inactiveTimeout = null;
self._setInactive();
}
}, 0, this);
]]></body>
</method>
<method name="_setActive">
<body><![CDATA[
if (this._inactiveTimeout) {
clearTimeout(this._inactiveTimeout);
this._inactiveTimeout = null;
}
this.removeAttribute("inactive");
]]></body>
</method>
</implementation>
<handlers>
<handler event="DOMMenuBarActive" action="this._setActive();"/>
<handler event="popupshowing" action="this._setActive();"/>
<handler event="mousedown" button="2" action="this._contextMenuListener.init(event);"/>
<handler event="DOMMenuBarInactive"><![CDATA[
if (!this._contextMenuListener.active)
this._setInactiveAsync();
]]></handler>
</handlers>
</binding>
<binding id="toolbar-drag"
extends="chrome://browser/content/customizableui/toolbar.xml#toolbar">
<implementation>
<field name="_dragBindingAlive">true</field>
<constructor><![CDATA[
if (!this._draggableStarted) {
this._draggableStarted = true;
try {
let tmp = {};
Components.utils.import("resource://gre/modules/WindowDraggingUtils.jsm", tmp);
let draggableThis = new tmp.WindowDraggingElement(this);
draggableThis.mouseDownCheck = function(e) {
return this._dragBindingAlive;
};
} catch (e) {}
}
]]></constructor>
</implementation>
</binding>
<!-- This is a peculiar binding. It is here to deal with overlayed/inserted add-on content,
and immediately direct such content elsewhere. -->
<binding id="addonbar-delegating">
<implementation>
<constructor><![CDATA[
// Reading these immediately so nobody messes with them anymore:
this._delegatingToolbar = this.getAttribute("toolbar-delegate");
this._wasCollapsed = this.getAttribute("collapsed") == "true";
// Leaving those in here to unbreak some code:
if (document.readyState == "complete") {
this._init();
} else {
// Need to wait until XUL overlays are loaded. See bug 554279.
let self = this;
document.addEventListener("readystatechange", function onReadyStateChange() {
if (document.readyState != "complete")
return;
document.removeEventListener("readystatechange", onReadyStateChange);
self._init();
});
}
]]></constructor>
<method name="_init">
<body><![CDATA[
// Searching for the toolbox palette in the toolbar binding because
// toolbars are constructed first.
let toolbox = this.toolbox;
if (toolbox && !toolbox.palette) {
for (let node of toolbox.children) {
if (node.localName == "toolbarpalette") {
// Hold on to the palette but remove it from the document.
toolbox.palette = node;
toolbox.removeChild(node);
}
}
}
// pass the current set of children for comparison with placements:
let children = [];
for (let node of this.childNodes) {
if (node.getAttribute("skipintoolbarset") != "true" && node.id) {
// Force everything to be removable so that buildArea can chuck stuff
// out if the user has customized things / we've been here before:
if (!this._whiteListed.has(node.id)) {
node.setAttribute("removable", "true");
}
children.push(node);
}
}
CustomizableUI.registerToolbarNode(this, children);
let existingMigratedItems = (this.getAttribute("migratedset") || "").split(",");
for (let migratedItem of existingMigratedItems.filter((x) => !!x)) {
this._currentSetMigrated.add(migratedItem);
}
this.evictNodes();
// We can't easily use |this| or strong bindings for the observer fn here
// because that creates leaky circular references when the node goes away,
// and XBL destructors are unreliable.
let mutationObserver = new MutationObserver(function(mutations) {
if (!mutations.length) {
return;
}
let toolbar = mutations[0].target;
// Can't use our own attribute because we might not have one if we're set to
// collapsed
let areCustomizing = toolbar.ownerDocument.documentElement.getAttribute("customizing");
if (!toolbar._isModifying && !areCustomizing) {
toolbar.evictNodes();
}
});
mutationObserver.observe(this, {childList: true});
]]></body>
</method>
<method name="evictNodes">
<body><![CDATA[
this._isModifying = true;
let i = this.childNodes.length;
while (i--) {
let node = this.childNodes[i];
if (this.childNodes[i].id) {
this.evictNode(this.childNodes[i]);
} else {
node.remove();
}
}
this._isModifying = false;
this._updateMigratedSet();
]]></body>
</method>
<method name="evictNode">
<parameter name="aNode"/>
<body>
<![CDATA[
if (this._whiteListed.has(aNode.id) || CustomizableUI.isSpecialWidget(aNode.id)) {
return;
}
const kItemMaxWidth = 100;
let oldParent = aNode.parentNode;
aNode.setAttribute("removable", "true");
this._currentSetMigrated.add(aNode.id);
let movedOut = false;
if (!this._wasCollapsed) {
try {
let nodeWidth = aNode.getBoundingClientRect().width;
if (nodeWidth == 0 || nodeWidth > kItemMaxWidth) {
throw new Error(aNode.id + " is too big (" + nodeWidth +
"px wide), moving to the palette");
}
CustomizableUI.addWidgetToArea(aNode.id, this._delegatingToolbar);
movedOut = true;
} catch (ex) {
// This will throw if the node is too big, or can't be moved there for
// some reason. Report this:
Cu.reportError(ex);
}
}
/* We won't have moved the widget if either the add-on bar was collapsed,
* or if it was too wide to be inserted into the navbar. */
if (!movedOut) {
try {
CustomizableUI.removeWidgetFromArea(aNode.id);
} catch (ex) {
Cu.reportError(ex);
aNode.remove();
}
}
// Surprise: addWidgetToArea(palette) will get you nothing if the palette
// is not constructed yet. Fix:
if (aNode.parentNode == oldParent) {
let palette = this.toolbox.palette;
if (palette && oldParent != palette) {
palette.appendChild(aNode);
}
}
]]></body>
</method>
<method name="insertItem">
<parameter name="aId"/>
<parameter name="aBeforeElt"/>
<parameter name="aWrapper"/>
<body><![CDATA[
if (aWrapper) {
Cu.reportError("Can't insert " + aId + ": using insertItem " +
"no longer supports wrapper elements.");
return null;
}
let widget = CustomizableUI.getWidget(aId);
widget = widget && widget.forWindow(window);
let node = widget && widget.node;
if (!node) {
return null;
}
this._isModifying = true;
// Temporarily add it here so it can have a width, then ditch it:
this.appendChild(node);
this.evictNode(node);
this._isModifying = false;
this._updateMigratedSet();
// We will now have moved stuff around; kick off some events
// so add-ons know we've just moved their stuff:
// XXXgijs: only in this window. It's hard to know for sure what's the right
// thing to do here - typically insertItem is used on each window, so
// this seems to make the most sense, even if some of the effects of
// evictNode might affect multiple windows.
CustomizableUI.dispatchToolboxEvent("customizationchange", {}, window);
CustomizableUI.dispatchToolboxEvent("aftercustomization", {}, window);
return node;
]]></body>
</method>
<method name="getMigratedItems">
<body><![CDATA[
return [...this._currentSetMigrated];
]]></body>
</method>
<method name="_updateMigratedSet">
<body><![CDATA[
let newMigratedItems = this.getMigratedItems().join(",");
if (this.getAttribute("migratedset") != newMigratedItems) {
this.setAttribute("migratedset", newMigratedItems);
this.ownerDocument.persist(this.id, "migratedset");
}
]]></body>
</method>
<property name="customizationTarget" readonly="true">
<getter><![CDATA[
return this;
]]></getter>
</property>
<property name="currentSet">
<getter><![CDATA[
return Array.from(this.children, node => node.id).join(",");
]]></getter>
<setter><![CDATA[
let v = val.split(",");
let newButtons = v.filter(x => x && (!this._whiteListed.has(x) &&
!CustomizableUI.isSpecialWidget(x) &&
!this._currentSetMigrated.has(x)));
for (let newButton of newButtons) {
this._currentSetMigrated.add(newButton);
this.insertItem(newButton);
}
this._updateMigratedSet();
]]></setter>
</property>
<property name="toolbox" readonly="true">
<getter><![CDATA[
if (!this._toolbox && this.parentNode &&
this.parentNode.localName == "toolbox") {
this._toolbox = this.parentNode;
}
return this._toolbox;
]]></getter>
</property>
<field name="_whiteListed" readonly="true">new Set(["addonbar-closebutton", "status-bar"])</field>
<field name="_isModifying">false</field>
<field name="_currentSetMigrated">new Set()</field>
</implementation>
</binding>
</bindings>