Issue #303 Part 1: Move basilisk files from /browser to /application/basilisk

This commit is contained in:
wolfbeast 2018-06-04 13:17:38 +02:00 • committed by Roy Tam
commit 177b28a898
1938 changed files with 0 additions and 0 deletions

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,420 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
Components.utils.import("resource:///modules/CustomizableUI.jsm");
var gManagers = new WeakMap();
const kPaletteId = "customization-palette";
const kPlaceholderClass = "panel-customization-placeholder";
this.EXPORTED_SYMBOLS = ["DragPositionManager"];
function AreaPositionManager(aContainer) {
// Caching the direction and bounds of the container for quick access later:
let window = aContainer.ownerGlobal;
this._dir = window.getComputedStyle(aContainer).direction;
let containerRect = aContainer.getBoundingClientRect();
this._containerInfo = {
left: containerRect.left,
right: containerRect.right,
top: containerRect.top,
width: containerRect.width
};
this._inPanel = aContainer.id == CustomizableUI.AREA_PANEL;
this._horizontalDistance = null;
this.update(aContainer);
}
AreaPositionManager.prototype = {
_nodePositionStore: null,
_wideCache: null,
update: function(aContainer) {
this._nodePositionStore = new WeakMap();
this._wideCache = new Set();
let last = null;
let singleItemHeight;
for (let child of aContainer.children) {
if (child.hidden) {
continue;
}
let isNodeWide = this._checkIfWide(child);
if (isNodeWide) {
this._wideCache.add(child.id);
}
let coordinates = this._lazyStoreGet(child);
// We keep a baseline horizontal distance between non-wide nodes around
// for use when we can't compare with previous/next nodes
if (!this._horizontalDistance && last && !isNodeWide) {
this._horizontalDistance = coordinates.left - last.left;
}
// We also keep the basic height of non-wide items for use below:
if (!isNodeWide && !singleItemHeight) {
singleItemHeight = coordinates.height;
}
last = !isNodeWide ? coordinates : null;
}
if (this._inPanel) {
this._heightToWidthFactor = CustomizableUI.PANEL_COLUMN_COUNT;
} else {
this._heightToWidthFactor = this._containerInfo.width / singleItemHeight;
}
},
/**
* Find the closest node in the container given the coordinates.
* "Closest" is defined in a somewhat strange manner: we prefer nodes
* which are in the same row over nodes that are in a different row.
* In order to implement this, we use a weighted cartesian distance
* where dy is more heavily weighted by a factor corresponding to the
* ratio between the container's width and the height of its elements.
*/
find: function(aContainer, aX, aY, aDraggedItemId) {
let closest = null;
let minCartesian = Number.MAX_VALUE;
let containerX = this._containerInfo.left;
let containerY = this._containerInfo.top;
for (let node of aContainer.children) {
let coordinates = this._lazyStoreGet(node);
let offsetX = coordinates.x - containerX;
let offsetY = coordinates.y - containerY;
let hDiff = offsetX - aX;
let vDiff = offsetY - aY;
// For wide widgets, we're always going to be further from the center
// horizontally. Compensate:
if (this.isWide(node)) {
hDiff /= CustomizableUI.PANEL_COLUMN_COUNT;
}
// Then compensate for the height/width ratio so that we prefer items
// which are in the same row:
hDiff /= this._heightToWidthFactor;
let cartesianDiff = hDiff * hDiff + vDiff * vDiff;
if (cartesianDiff < minCartesian) {
minCartesian = cartesianDiff;
closest = node;
}
}
// Now correct this node based on what we're dragging
if (closest) {
let doc = aContainer.ownerDocument;
let draggedItem = doc.getElementById(aDraggedItemId);
// If dragging a wide item, always pick the first item in a row:
if (this._inPanel && draggedItem &&
draggedItem.classList.contains(CustomizableUI.WIDE_PANEL_CLASS)) {
return this._firstInRow(closest);
}
let targetBounds = this._lazyStoreGet(closest);
let farSide = this._dir == "ltr" ? "right" : "left";
let outsideX = targetBounds[farSide];
// Check if we're closer to the next target than to this one:
// Only move if we're not targeting a node in a different row:
if (aY > targetBounds.top && aY < targetBounds.bottom) {
if ((this._dir == "ltr" && aX > outsideX) ||
(this._dir == "rtl" && aX < outsideX)) {
return closest.nextSibling || aContainer;
}
}
}
return closest;
},
/**
* "Insert" a "placeholder" by shifting the subsequent children out of the
* way. We go through all the children, and shift them based on the position
* they would have if we had inserted something before aBefore. We use CSS
* transforms for this, which are CSS transitioned.
*/
insertPlaceholder: function(aContainer, aBefore, aWide, aSize, aIsFromThisArea) {
let isShifted = false;
let shiftDown = aWide;
for (let child of aContainer.children) {
// Don't need to shift hidden nodes:
if (child.getAttribute("hidden") == "true") {
continue;
}
// If this is the node before which we're inserting, start shifting
// everything that comes after. One exception is inserting at the end
// of the menupanel, in which case we do not shift the placeholders:
if (child == aBefore && !child.classList.contains(kPlaceholderClass)) {
isShifted = true;
// If the node before which we're inserting is wide, we should
// shift everything one row down:
if (!shiftDown && this.isWide(child)) {
shiftDown = true;
}
}
// If we're moving items before a wide node that were already there,
// it's possible it's not necessary to shift nodes
// including & after the wide node.
if (this.__undoShift) {
isShifted = false;
}
if (isShifted) {
// Conversely, if we're adding something before a wide node, for
// simplicity's sake we move everything including the wide node down:
if (this.__moveDown) {
shiftDown = true;
}
if (aIsFromThisArea && !this._lastPlaceholderInsertion) {
child.setAttribute("notransition", "true");
}
// Determine the CSS transform based on the next node:
child.style.transform = this._getNextPos(child, shiftDown, aSize);
} else {
// If we're not shifting this node, reset the transform
child.style.transform = "";
}
}
if (aContainer.lastChild && aIsFromThisArea &&
!this._lastPlaceholderInsertion) {
// Flush layout:
aContainer.lastChild.getBoundingClientRect();
// then remove all the [notransition]
for (let child of aContainer.children) {
child.removeAttribute("notransition");
}
}
delete this.__moveDown;
delete this.__undoShift;
this._lastPlaceholderInsertion = aBefore;
},
isWide: function(aNode) {
return this._wideCache.has(aNode.id);
},
_checkIfWide: function(aNode) {
return this._inPanel && aNode && aNode.firstChild &&
aNode.firstChild.classList.contains(CustomizableUI.WIDE_PANEL_CLASS);
},
/**
* Reset all the transforms in this container, optionally without
* transitioning them.
* @param aContainer the container in which to reset transforms
* @param aNoTransition if truthy, adds a notransition attribute to the node
* while resetting the transform.
*/
clearPlaceholders: function(aContainer, aNoTransition) {
for (let child of aContainer.children) {
if (aNoTransition) {
child.setAttribute("notransition", true);
}
child.style.transform = "";
if (aNoTransition) {
// Need to force a reflow otherwise this won't work.
child.getBoundingClientRect();
child.removeAttribute("notransition");
}
}
// We snapped back, so we can assume there's no more
// "last" placeholder insertion point to keep track of.
if (aNoTransition) {
this._lastPlaceholderInsertion = null;
}
},
_getNextPos: function(aNode, aShiftDown, aSize) {
// Shifting down is easy:
if (this._inPanel && aShiftDown) {
return "translate(0, " + aSize.height + "px)";
}
return this._diffWithNext(aNode, aSize);
},
_diffWithNext: function(aNode, aSize) {
let xDiff;
let yDiff = null;
let nodeBounds = this._lazyStoreGet(aNode);
let side = this._dir == "ltr" ? "left" : "right";
let next = this._getVisibleSiblingForDirection(aNode, "next");
// First we determine the transform along the x axis.
// Usually, there will be a next node to base this on:
if (next) {
let otherBounds = this._lazyStoreGet(next);
xDiff = otherBounds[side] - nodeBounds[side];
// If the next node is a wide item in the panel, check if we could maybe
// just move further out in the same row, without snapping to the next
// one. This happens, for example, if moving an item that's before a wide
// node within its own row of items. There will be space to drop this
// item within the row, and the rest of the items do not need to shift.
if (this.isWide(next)) {
let otherXDiff = this._moveNextBasedOnPrevious(aNode, nodeBounds,
this._firstInRow(aNode));
// If this has the same sign as our original shift, we're still
// snapping to the start of the row. In this case, we should move
// everything after us a row down, so as not to display two nodes on
// top of each other:
// (we would be able to get away with checking for equality instead of
// equal signs here, but one of these is based on the x coordinate of
// the first item in row N and one on that for row N - 1, so this is
// safer, as their margins might differ)
if ((otherXDiff < 0) == (xDiff < 0)) {
this.__moveDown = true;
} else {
// Otherwise, we succeeded and can move further out. This also means
// we can stop shifting the rest of the content:
xDiff = otherXDiff;
this.__undoShift = true;
}
} else {
// We set this explicitly because otherwise some strange difference
// between the height and the actual difference between line creeps in
// and messes with alignments
yDiff = otherBounds.top - nodeBounds.top;
}
} else {
// We don't have a sibling whose position we can use. First, let's see
// if we're also the first item (which complicates things):
let firstNode = this._firstInRow(aNode);
if (aNode == firstNode) {
// Maybe we stored the horizontal distance between non-wide nodes,
// if not, we'll use the width of the incoming node as a proxy:
xDiff = this._horizontalDistance || aSize.width;
} else {
// If not, we should be able to get the distance to the previous node
// and use the inverse, unless there's no room for another node (ie we
// are the last node and there's no room for another one)
xDiff = this._moveNextBasedOnPrevious(aNode, nodeBounds, firstNode);
}
}
// If we've not determined the vertical difference yet, check it here
if (yDiff === null) {
// If the next node is behind rather than in front, we must have moved
// vertically:
if ((xDiff > 0 && this._dir == "rtl") || (xDiff < 0 && this._dir == "ltr")) {
yDiff = aSize.height;
} else {
// Otherwise, we haven't
yDiff = 0;
}
}
return "translate(" + xDiff + "px, " + yDiff + "px)";
},
/**
* Helper function to find the transform a node if there isn't a next node
* to base that on.
* @param aNode the node to transform
* @param aNodeBounds the bounding rect info of this node
* @param aFirstNodeInRow the first node in aNode's row
*/
_moveNextBasedOnPrevious: function(aNode, aNodeBounds, aFirstNodeInRow) {
let next = this._getVisibleSiblingForDirection(aNode, "previous");
let otherBounds = this._lazyStoreGet(next);
let side = this._dir == "ltr" ? "left" : "right";
let xDiff = aNodeBounds[side] - otherBounds[side];
// If, however, this means we move outside the container's box
// (i.e. the row in which this item is placed is full)
// we should move it to align with the first item in the next row instead
let bound = this._containerInfo[this._dir == "ltr" ? "right" : "left"];
if ((this._dir == "ltr" && xDiff + aNodeBounds.right > bound) ||
(this._dir == "rtl" && xDiff + aNodeBounds.left < bound)) {
xDiff = this._lazyStoreGet(aFirstNodeInRow)[side] - aNodeBounds[side];
}
return xDiff;
},
/**
* Get position details from our cache. If the node is not yet cached, get its position
* information and cache it now.
* @param aNode the node whose position info we want
* @return the position info
*/
_lazyStoreGet: function(aNode) {
let rect = this._nodePositionStore.get(aNode);
if (!rect) {
// getBoundingClientRect() returns a DOMRect that is live, meaning that
// as the element moves around, the rects values change. We don't want
// that - we want a snapshot of what the rect values are right at this
// moment, and nothing else. So we have to clone the values.
let clientRect = aNode.getBoundingClientRect();
rect = {
left: clientRect.left,
right: clientRect.right,
width: clientRect.width,
height: clientRect.height,
top: clientRect.top,
bottom: clientRect.bottom,
};
rect.x = rect.left + rect.width / 2;
rect.y = rect.top + rect.height / 2;
Object.freeze(rect);
this._nodePositionStore.set(aNode, rect);
}
return rect;
},
_firstInRow: function(aNode) {
// XXXmconley: I'm not entirely sure why we need to take the floor of these
// values - it looks like, periodically, we're getting fractional pixels back
// from lazyStoreGet. I've filed bug 994247 to investigate.
let bound = Math.floor(this._lazyStoreGet(aNode).top);
let rv = aNode;
let prev;
while (rv && (prev = this._getVisibleSiblingForDirection(rv, "previous"))) {
if (Math.floor(this._lazyStoreGet(prev).bottom) <= bound) {
return rv;
}
rv = prev;
}
return rv;
},
_getVisibleSiblingForDirection: function(aNode, aDirection) {
let rv = aNode;
do {
rv = rv[aDirection + "Sibling"];
} while (rv && rv.getAttribute("hidden") == "true")
return rv;
}
}
var DragPositionManager = {
start: function(aWindow) {
let areas = CustomizableUI.areas.filter((area) => CustomizableUI.getAreaType(area) != "toolbar");
areas = areas.map((area) => CustomizableUI.getCustomizeTargetForArea(area, aWindow));
areas.push(aWindow.document.getElementById(kPaletteId));
for (let areaNode of areas) {
let positionManager = gManagers.get(areaNode);
if (positionManager) {
positionManager.update(areaNode);
} else {
gManagers.set(areaNode, new AreaPositionManager(areaNode));
}
}
},
add: function(aWindow, aArea, aContainer) {
if (CustomizableUI.getAreaType(aArea) != "toolbar") {
return;
}
gManagers.set(aContainer, new AreaPositionManager(aContainer));
},
remove: function(aWindow, aArea, aContainer) {
if (CustomizableUI.getAreaType(aArea) != "toolbar") {
return;
}
gManagers.delete(aContainer);
},
stop: function() {
gManagers = new WeakMap();
},
getManagerForArea: function(aArea) {
return gManagers.get(aArea);
}
};
Object.freeze(DragPositionManager);

View file

@ -0,0 +1,172 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
this.EXPORTED_SYMBOLS = ["PanelWideWidgetTracker"];
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
var gPanel = CustomizableUI.AREA_PANEL;
// We keep track of the widget placements for the panel locally:
var gPanelPlacements = [];
// All the wide widgets we know of:
var gWideWidgets = new Set();
// All the widgets we know of:
var gSeenWidgets = new Set();
var PanelWideWidgetTracker = {
// Listeners used to validate panel contents whenever they change:
onWidgetAdded: function(aWidgetId, aArea, aPosition) {
if (aArea == gPanel) {
gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel);
let moveForward = this.shouldMoveForward(aWidgetId, aPosition);
this.adjustWidgets(aWidgetId, moveForward);
}
},
onWidgetMoved: function(aWidgetId, aArea, aOldPosition, aNewPosition) {
if (aArea == gPanel) {
gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel);
let moveForward = this.shouldMoveForward(aWidgetId, aNewPosition);
this.adjustWidgets(aWidgetId, moveForward);
}
},
onWidgetRemoved: function(aWidgetId, aPrevArea) {
if (aPrevArea == gPanel) {
gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel);
this.adjustWidgets(aWidgetId, false);
}
},
onWidgetReset: function(aWidgetId) {
gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel);
},
// Listener to keep abreast of any new nodes. We use the DOM one because
// we need access to the actual node's classlist, so we can't use the ones above.
// Furthermore, onWidgetCreated only fires for API-based widgets, not for XUL ones.
onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer) {
if (!gSeenWidgets.has(aNode.id)) {
if (aNode.classList.contains(CustomizableUI.WIDE_PANEL_CLASS)) {
gWideWidgets.add(aNode.id);
}
gSeenWidgets.add(aNode.id);
}
},
// When widgets get destroyed, we remove them from our sets of stuff we care about:
onWidgetDestroyed: function(aWidgetId) {
gSeenWidgets.delete(aWidgetId);
gWideWidgets.delete(aWidgetId);
},
shouldMoveForward: function(aWidgetId, aPosition) {
let currentWidgetAtPosition = gPanelPlacements[aPosition + 1];
let rv = gWideWidgets.has(currentWidgetAtPosition) && !gWideWidgets.has(aWidgetId);
// We might now think we can move forward, but for that we need at least 2 more small
// widgets to be present:
if (rv) {
let furtherWidgets = gPanelPlacements.slice(aPosition + 2);
let realWidgets = 0;
if (furtherWidgets.length >= 2) {
while (furtherWidgets.length && realWidgets < 2) {
let w = furtherWidgets.shift();
if (!gWideWidgets.has(w) && this.checkWidgetStatus(w)) {
realWidgets++;
}
}
}
if (realWidgets < 2) {
rv = false;
}
}
return rv;
},
adjustWidgets: function(aWidgetId, aMoveForwards) {
if (this.adjusting) {
return;
}
this.adjusting = true;
let widgetsAffected = gPanelPlacements.filter((w) => gWideWidgets.has(w));
// If we're moving the wide widgets forwards (down/to the right in the panel)
// we want to start with the last widgets. Otherwise we move widgets over other wide
// widgets, which might mess up their order. Likewise, if moving backwards we should start with
// the first widget and work our way down/right from there.
let compareFn = aMoveForwards ? ((a, b) => a < b) : ((a, b) => a > b);
widgetsAffected.sort((a, b) => compareFn(gPanelPlacements.indexOf(a),
gPanelPlacements.indexOf(b)));
for (let widget of widgetsAffected) {
this.adjustPosition(widget, aMoveForwards);
}
this.adjusting = false;
},
// This function is called whenever an item gets moved in the menu panel. It
// adjusts the position of widgets within the panel to prevent "gaps" between
// wide widgets that could be filled up with single column widgets
adjustPosition: function(aWidgetId, aMoveForwards) {
// Make sure that there are n % columns = 0 narrow buttons before the widget.
let placementIndex = gPanelPlacements.indexOf(aWidgetId);
let prevSiblingCount = 0;
let fixedPos = null;
while (placementIndex--) {
let thisWidgetId = gPanelPlacements[placementIndex];
if (gWideWidgets.has(thisWidgetId)) {
continue;
}
let widgetStatus = this.checkWidgetStatus(thisWidgetId);
if (!widgetStatus) {
continue;
}
if (widgetStatus == "public-only") {
fixedPos = !fixedPos ? placementIndex : Math.min(fixedPos, placementIndex);
prevSiblingCount = 0;
} else {
prevSiblingCount++;
}
}
if (fixedPos !== null || prevSiblingCount % CustomizableUI.PANEL_COLUMN_COUNT) {
let desiredPos = (fixedPos !== null) ? fixedPos : gPanelPlacements.indexOf(aWidgetId);
let desiredChange = -(prevSiblingCount % CustomizableUI.PANEL_COLUMN_COUNT);
if (aMoveForwards && fixedPos == null) {
// +1 because otherwise we'd count ourselves:
desiredChange = CustomizableUI.PANEL_COLUMN_COUNT + desiredChange + 1;
}
desiredPos += desiredChange;
CustomizableUI.moveWidgetWithinArea(aWidgetId, desiredPos);
}
},
/*
* Check whether a widget id is actually known anywhere.
* @returns false if the widget doesn't exist,
* "public-only" if it's not shown in private windows
* "real" if it does exist and is shown even in private windows
*/
checkWidgetStatus: function(aWidgetId) {
let widgetWrapper = CustomizableUI.getWidget(aWidgetId);
// This widget might not actually exist:
if (!widgetWrapper) {
return false;
}
// This widget might still not actually exist:
if (widgetWrapper.provider == CustomizableUI.PROVIDER_XUL &&
widgetWrapper.instances.length == 0) {
return false;
}
// Or it might only be there some of the time:
if (widgetWrapper.provider == CustomizableUI.PROVIDER_API &&
widgetWrapper.showInPrivateBrowsing === false) {
return "public-only";
}
return "real";
},
init: function() {
// Initialize our local placements copy and register the listener
gPanelPlacements = CustomizableUI.getWidgetIdsInArea(gPanel);
CustomizableUI.addListener(this);
},
};

View file

@ -0,0 +1,65 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
"use strict";
this.EXPORTED_SYMBOLS = ["ScrollbarSampler"];
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
Cu.import("resource://gre/modules/Services.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
var gSystemScrollbarWidth = null;
this.ScrollbarSampler = {
getSystemScrollbarWidth: function() {
if (gSystemScrollbarWidth !== null) {
return Promise.resolve(gSystemScrollbarWidth);
}
return new Promise(resolve => {
this._sampleSystemScrollbarWidth().then(function(systemScrollbarWidth) {
gSystemScrollbarWidth = systemScrollbarWidth;
resolve(gSystemScrollbarWidth);
});
});
},
resetSystemScrollbarWidth: function() {
gSystemScrollbarWidth = null;
},
_sampleSystemScrollbarWidth: function() {
let hwin = Services.appShell.hiddenDOMWindow;
let hdoc = hwin.document.documentElement;
let iframe = hwin.document.createElementNS("http://www.w3.org/1999/xhtml",
"html:iframe");
iframe.setAttribute("srcdoc", '<body style="overflow-y: scroll"></body>');
hdoc.appendChild(iframe);
let cwindow = iframe.contentWindow;
let utils = cwindow.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils);
return new Promise(resolve => {
cwindow.addEventListener("load", function onLoad(aEvent) {
cwindow.removeEventListener("load", onLoad);
let sbWidth = {};
try {
utils.getScrollbarSize(true, sbWidth, {});
} catch (e) {
Cu.reportError("Could not sample scrollbar size: " + e + " -- " +
e.stack);
sbWidth.value = 0;
}
// Minimum width of 10 so that we have enough padding:
sbWidth.value = Math.max(sbWidth.value, 10);
resolve(sbWidth.value);
iframe.remove();
});
});
}
};
Object.freeze(this.ScrollbarSampler);

View file

@ -0,0 +1,82 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<hbox id="customization-container" flex="1" hidden="true">
<vbox flex="1" id="customization-palette-container">
<label id="customization-header">
&customizeMode.menuAndToolbars.header2;
</label>
<hbox id="customization-empty" hidden="true">
<label>&customizeMode.menuAndToolbars.empty;</label>
<label onclick="BrowserOpenAddonsMgr('addons://discover/');"
onkeypress="BrowserOpenAddonsMgr('addons://discover/');"
id="customization-more-tools"
class="text-link">
&customizeMode.menuAndToolbars.emptyLink;
</label>
</hbox>
<vbox id="customization-palette" class="customization-palette"/>
<spacer id="customization-spacer"/>
<hbox id="customization-footer">
#ifdef CAN_DRAW_IN_TITLEBAR
<button id="customization-titlebar-visibility-button" class="customizationmode-button"
label="&customizeMode.titlebar;" type="checkbox"
#NB: because oncommand fires after click, by the time we've fired, the checkbox binding
# will already have switched the button's state, so this is correct:
oncommand="gCustomizeMode.toggleTitlebar(this.hasAttribute('checked'))"/>
#endif
<button id="customization-toolbar-visibility-button" label="&customizeMode.toolbars;" class="customizationmode-button" type="menu">
<menupopup id="customization-toolbar-menu" onpopupshowing="onViewToolbarsPopupShowing(event)"/>
</button>
<button id="customization-lwtheme-button" label="&customizeMode.lwthemes;" class="customizationmode-button" type="menu">
<panel type="arrow" id="customization-lwtheme-menu"
onpopupshowing="gCustomizeMode.onLWThemesMenuShowing(event);"
position="topcenter bottomleft"
flip="none"
role="menu">
<label id="customization-lwtheme-menu-header" value="&customizeMode.lwthemes.myThemes;"/>
<label id="customization-lwtheme-menu-recommended" value="&customizeMode.lwthemes.recommended;"/>
<hbox id="customization-lwtheme-menu-footer">
<toolbarbutton class="customization-lwtheme-menu-footeritem"
label="&customizeMode.lwthemes.menuManage;"
accesskey="&customizeMode.lwthemes.menuManage.accessKey;"
tabindex="0"
oncommand="gCustomizeMode.openAddonsManagerThemes(event);"/>
<toolbarbutton class="customization-lwtheme-menu-footeritem"
label="&customizeMode.lwthemes.menuGetMore;"
accesskey="&customizeMode.lwthemes.menuGetMore.accessKey;"
tabindex="0"
oncommand="gCustomizeMode.getMoreThemes(event);"/>
</hbox>
</panel>
</button>
<spacer id="customization-footer-spacer"/>
<button id="customization-undo-reset-button"
class="customizationmode-button"
hidden="true"
oncommand="gCustomizeMode.undoReset();"
label="&undoCmd.label;"/>
<button id="customization-reset-button"
oncommand="gCustomizeMode.reset();"
label="&customizeMode.restoreDefaults;"
class="customizationmode-button"/>
</hbox>
</vbox>
<vbox id="customization-panel-container">
<vbox id="customization-panelWrapper">
<html:style html:type="text/html" scoped="scoped">
@import url(chrome://global/skin/popup.css);
</html:style>
<box class="panel-arrowbox">
<box flex="1"/>
<image class="panel-arrow" side="top"/>
</box>
<box class="panel-arrowcontent" side="top" flex="1">
<hbox id="customization-panelHolder"/>
<box class="panel-inner-arrowcontentfooter" hidden="true"/>
</box>
</vbox>
</vbox>
</hbox>

View file

@ -0,0 +1,10 @@
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
browser.jar:
content/browser/customizableui/panelUI.css
content/browser/customizableui/panelUI.js
content/browser/customizableui/panelUI.xml
content/browser/customizableui/toolbar.xml

View file

@ -0,0 +1,7 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
JAR_MANIFESTS += ['jar.mn']

View file

@ -0,0 +1,31 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
.panel-viewstack[viewtype="main"] > .panel-clickcapturer {
pointer-events: none;
}
.panel-mainview,
.panel-viewcontainer,
.panel-viewstack {
overflow: hidden;
}
.panel-viewstack {
position: relative;
}
.panel-subviews {
-moz-stack-sizing: ignore;
transform: translateX(0);
overflow-y: auto;
}
.panel-subviews[panelopen] {
transition: transform var(--panelui-subview-transition-duration);
}
.panel-viewcontainer[panelopen]:-moz-any(:not([viewtype="main"]),[transitioning="true"]) {
transition: height var(--panelui-subview-transition-duration);
}

View file

@ -0,0 +1,386 @@
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<panel id="PanelUI-popup"
role="group"
type="arrow"
hidden="true"
flip="slide"
position="bottomcenter topright"
noautofocus="true">
<panelmultiview id="PanelUI-multiView" mainViewId="PanelUI-mainView">
<panelview id="PanelUI-mainView" context="customizationPanelContextMenu">
<vbox id="PanelUI-contents-scroller">
<vbox id="PanelUI-contents" class="panelUI-grid"/>
</vbox>
<footer id="PanelUI-footer">
<toolbarbutton id="PanelUI-update-status"
oncommand="gMenuButtonUpdateBadge.onMenuPanelCommand(event);"
wrap="true"
hidden="true"/>
<hbox id="PanelUI-footer-fxa">
<hbox id="PanelUI-fxa-status"
defaultlabel="&fxaSignIn.label;"
signedinTooltiptext="&fxaSignedIn.tooltip;"
tooltiptext="&fxaSignedIn.tooltip;"
errorlabel="&fxaSignInError.label;"
unverifiedlabel="&fxaUnverified.label;"
onclick="if (event.which == 1) gFxAccounts.onMenuPanelCommand();">
<image id="PanelUI-fxa-avatar"/>
<toolbarbutton id="PanelUI-fxa-label"
fxabrandname="&syncBrand.fxAccount.label;"/>
</hbox>
<toolbarseparator/>
<toolbarbutton id="PanelUI-fxa-icon"
oncommand="gSyncUI.doSync();"
closemenu="none">
<observes element="sync-status" attribute="syncstatus"/>
<observes element="sync-status" attribute="tooltiptext"/>
</toolbarbutton>
</hbox>
<hbox id="PanelUI-footer-inner">
<toolbarbutton id="PanelUI-customize" label="&appMenuCustomize.label;"
exitLabel="&appMenuCustomizeExit.label;"
tooltiptext="&appMenuCustomize.tooltip;"
exitTooltiptext="&appMenuCustomizeExit.tooltip;"
closemenu="none"
oncommand="gCustomizeMode.toggle();"/>
<toolbarseparator/>
<toolbarbutton id="PanelUI-help" label="&helpMenu.label;"
closemenu="none"
tooltiptext="&appMenuHelp.tooltip;"
oncommand="PanelUI.showHelpView(this);"/>
<toolbarseparator/>
<toolbarbutton id="PanelUI-quit"
#ifdef XP_WIN
label="&quitApplicationCmdWin2.label;"
tooltiptext="&quitApplicationCmdWin2.tooltip;"
#else
#ifdef XP_MACOSX
label="&quitApplicationCmdMac2.label;"
#else
label="&quitApplicationCmd.label;"
#endif
#endif
command="cmd_quitApplication"/>
</hbox>
</footer>
</panelview>
<panelview id="PanelUI-history" flex="1">
<label value="&appMenuHistory.label;" class="panel-subview-header"/>
<vbox class="panel-subview-body">
<toolbarbutton id="appMenuViewHistorySidebar"
label="&appMenuHistory.viewSidebar.label;"
type="checkbox"
class="subviewbutton"
key="key_gotoHistory"
oncommand="SidebarUI.toggle('viewHistorySidebar'); PanelUI.hide();">
<observes element="viewHistorySidebar" attribute="checked"/>
</toolbarbutton>
<toolbarbutton id="appMenuClearRecentHistory"
label="&appMenuHistory.clearRecent.label;"
class="subviewbutton"
command="Tools:Sanitize"/>
<toolbarbutton id="appMenuRestoreLastSession"
label="&appMenuHistory.restoreSession.label;"
class="subviewbutton"
command="Browser:RestoreLastSession"/>
<menuseparator id="PanelUI-recentlyClosedTabs-separator"/>
<vbox id="PanelUI-recentlyClosedTabs" tooltip="bhTooltip"/>
<menuseparator id="PanelUI-recentlyClosedWindows-separator"/>
<vbox id="PanelUI-recentlyClosedWindows" tooltip="bhTooltip"/>
<menuseparator id="PanelUI-historyItems-separator"/>
<vbox id="PanelUI-historyItems" tooltip="bhTooltip"/>
</vbox>
<toolbarbutton id="PanelUI-historyMore"
class="panel-subview-footer subviewbutton"
label="&appMenuHistory.showAll.label;"
oncommand="PlacesCommandHook.showPlacesOrganizer('History'); CustomizableUI.hidePanelForNode(this);"/>
</panelview>
<panelview id="PanelUI-remotetabs" flex="1" class="PanelUI-subView">
<label value="&appMenuRemoteTabs.label;" class="panel-subview-header"/>
<vbox class="panel-subview-body">
<!-- this widget has 3 boxes in the body, but only 1 is ever visible -->
<!-- When Sync is ready to sync -->
<vbox id="PanelUI-remotetabs-main" observes="sync-syncnow-state">
<vbox id="PanelUI-remotetabs-buttons">
<toolbarbutton id="PanelUI-remotetabs-view-sidebar"
class="subviewbutton"
observes="viewTabsSidebar"
label="&appMenuRemoteTabs.sidebar.label;"/>
<toolbarbutton id="PanelUI-remotetabs-syncnow"
observes="sync-status"
class="subviewbutton"
oncommand="gSyncUI.doSync();"
closemenu="none"/>
<menuseparator id="PanelUI-remotetabs-separator"/>
</vbox>
<deck id="PanelUI-remotetabs-deck">
<!-- Sync is ready to Sync and the "tabs" engine is enabled -->
<vbox id="PanelUI-remotetabs-tabspane">
<vbox id="PanelUI-remotetabs-tabslist"
notabsforclientlabel="&appMenuRemoteTabs.notabs.label;"
/>
</vbox>
<!-- Sync is ready to Sync but the "tabs" engine isn't enabled-->
<hbox id="PanelUI-remotetabs-tabsdisabledpane" pack="center" flex="1">
<vbox class="PanelUI-remotetabs-instruction-box">
<hbox pack="center">
<image class="fxaSyncIllustration" alt=""/>
</hbox>
<label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.tabsnotsyncing.label;</label>
<hbox pack="center">
<toolbarbutton class="PanelUI-remotetabs-prefs-button"
label="&appMenuRemoteTabs.openprefs.label;"
oncommand="gSyncUI.openSetup(null, 'synced-tabs');"/>
</hbox>
</vbox>
</hbox>
<!-- Sync is ready to Sync but we are still fetching the tabs to show -->
<vbox id="PanelUI-remotetabs-fetching">
<!-- Show intentionally blank panel, see bug 1239845 -->
</vbox>
<!-- Sync has only 1 (ie, this) device connected -->
<hbox id="PanelUI-remotetabs-nodevicespane" pack="center" flex="1">
<vbox class="PanelUI-remotetabs-instruction-box">
<hbox pack="center">
<image class="fxaSyncIllustration" alt=""/>
</hbox>
<label class="PanelUI-remotetabs-instruction-title">&appMenuRemoteTabs.noclients.title;</label>
<label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.noclients.subtitle;</label>
<!-- The inner HTML for PanelUI-remotetabs-mobile-promo is built at runtime -->
<label id="PanelUI-remotetabs-mobile-promo" fxAccountsBrand="&syncBrand.fxAccount.label;"/>
</vbox>
</hbox>
</deck>
</vbox>
<!-- a box to ensure contained boxes are centered horizonally -->
<hbox pack="center" flex="1">
<!-- When Sync is not configured -->
<vbox id="PanelUI-remotetabs-setupsync"
flex="1"
align="center"
class="PanelUI-remotetabs-instruction-box"
observes="sync-setup-state">
<image class="fxaSyncIllustration" alt=""/>
<label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.notsignedin.label;</label>
<toolbarbutton class="PanelUI-remotetabs-prefs-button"
label="&appMenuRemoteTabs.signin.label;"
oncommand="gSyncUI.openSetup(null, 'synced-tabs');"/>
</vbox>
<!-- When Sync needs re-authentication. This uses the exact same messaging
as "Sync is not configured" but remains a separate box so we get
the goodness of observing broadcasters to manage the hidden states -->
<vbox id="PanelUI-remotetabs-reauthsync"
flex="1"
align="center"
class="PanelUI-remotetabs-instruction-box"
observes="sync-reauth-state">
<image class="fxaSyncIllustration" alt=""/>
<label class="PanelUI-remotetabs-instruction-label">&appMenuRemoteTabs.notsignedin.label;</label>
<toolbarbutton class="PanelUI-remotetabs-prefs-button"
label="&appMenuRemoteTabs.signin.label;"
oncommand="gSyncUI.openSetup(null, 'synced-tabs');"/>
</vbox>
</hbox>
</vbox>
</panelview>
<panelview id="PanelUI-bookmarks" flex="1" class="PanelUI-subView">
<label value="&bookmarksMenu.label;" class="panel-subview-header"/>
<vbox class="panel-subview-body">
<toolbarbutton id="panelMenuBookmarkThisPage"
class="subviewbutton"
observes="bookmarkThisPageBroadcaster"
command="Browser:AddBookmarkAs"
onclick="PanelUI.hide();"/>
<toolbarseparator/>
<toolbarbutton id="panelMenu_viewBookmarksSidebar"
label="&viewBookmarksSidebar2.label;"
class="subviewbutton"
key="viewBookmarksSidebarKb"
oncommand="SidebarUI.toggle('viewBookmarksSidebar'); PanelUI.hide();">
<observes element="viewBookmarksSidebar" attribute="checked"/>
</toolbarbutton>
<toolbarbutton id="panelMenu_viewBookmarksToolbar"
label="&viewBookmarksToolbar.label;"
type="checkbox"
toolbarId="PersonalToolbar"
class="subviewbutton"
oncommand="onViewToolbarCommand(event); PanelUI.hide();"/>
<toolbarseparator/>
<toolbarbutton id="panelMenu_bookmarksToolbar"
label="&personalbarCmd.label;"
class="subviewbutton cui-withicon"
oncommand="PlacesCommandHook.showPlacesOrganizer('BookmarksToolbar'); PanelUI.hide();"/>
<toolbarbutton id="panelMenu_unsortedBookmarks"
label="&otherBookmarksCmd.label;"
class="subviewbutton cui-withicon"
oncommand="PlacesCommandHook.showPlacesOrganizer('UnfiledBookmarks'); PanelUI.hide();"/>
<toolbarseparator class="small-separator"/>
<toolbaritem id="panelMenu_bookmarksMenu"
orient="vertical"
smoothscroll="false"
onclick="if (event.button == 1) BookmarkingUI.onPanelMenuViewCommand(event, this._placesView);"
oncommand="BookmarkingUI.onPanelMenuViewCommand(event, this._placesView);"
flatList="true"
tooltip="bhTooltip">
<!-- bookmarks menu items will go here -->
</toolbaritem>
</vbox>
<toolbarbutton id="panelMenu_showAllBookmarks"
label="&showAllBookmarks2.label;"
class="subviewbutton panel-subview-footer"
command="Browser:ShowAllBookmarks"
onclick="PanelUI.hide();"/>
</panelview>
<panelview id="PanelUI-feeds" flex="1" oncommand="FeedHandler.subscribeToFeed(null, event);">
<label value="&feedsMenu2.label;" class="panel-subview-header"/>
</panelview>
<panelview id="PanelUI-containers" flex="1">
<label value="&containersMenu.label;" class="panel-subview-header"/>
<vbox id="PanelUI-containersItems"/>
</panelview>
<panelview id="PanelUI-helpView" flex="1" class="PanelUI-subView">
<label value="&helpMenu.label;" class="panel-subview-header"/>
<vbox id="PanelUI-helpItems" class="panel-subview-body"/>
</panelview>
<panelview id="PanelUI-developer" flex="1">
<label value="&webDeveloperMenu.label;" class="panel-subview-header"/>
<vbox id="PanelUI-developerItems" class="panel-subview-body"/>
</panelview>
<panelview id="PanelUI-sidebar" flex="1">
<label value="&appMenuSidebars.label;" class="panel-subview-header"/>
<vbox id="PanelUI-sidebarItems" class="panel-subview-body"/>
</panelview>
<panelview id="PanelUI-characterEncodingView" flex="1">
<label value="&charsetMenu2.label;" class="panel-subview-header"/>
<vbox class="panel-subview-body">
<vbox id="PanelUI-characterEncodingView-pinned"
class="PanelUI-characterEncodingView-list"/>
<toolbarseparator/>
<vbox id="PanelUI-characterEncodingView-charsets"
class="PanelUI-characterEncodingView-list"/>
<toolbarseparator/>
<vbox>
<label id="PanelUI-characterEncodingView-autodetect-label"/>
<vbox id="PanelUI-characterEncodingView-autodetect"
class="PanelUI-characterEncodingView-list"/>
</vbox>
</vbox>
</panelview>
<panelview id="PanelUI-panicView" flex="1">
<vbox class="panel-subview-body">
<hbox id="PanelUI-panic-timeframe">
<image id="PanelUI-panic-timeframe-icon" alt=""/>
<vbox flex="1">
<hbox id="PanelUI-panic-header">
<image id="PanelUI-panic-timeframe-icon-small" alt=""/>
<description id="PanelUI-panic-mainDesc" flex="1">&panicButton.view.mainTimeframeDesc;</description>
</hbox>
<radiogroup id="PanelUI-panic-timeSpan" aria-labelledby="PanelUI-panic-mainDesc" closemenu="none">
<radio id="PanelUI-panic-5min" label="&panicButton.view.5min;" selected="true"
value="5" class="subviewradio"/>
<radio id="PanelUI-panic-2hr" label="&panicButton.view.2hr;"
value="2" class="subviewradio"/>
<radio id="PanelUI-panic-day" label="&panicButton.view.day;"
value="6" class="subviewradio"/>
</radiogroup>
</vbox>
</hbox>
<vbox id="PanelUI-panic-explanations">
<label id="PanelUI-panic-actionlist-main-label">&panicButton.view.mainActionDesc;</label>
<label id="PanelUI-panic-actionlist-windows" class="PanelUI-panic-actionlist">&panicButton.view.deleteTabsAndWindows;</label>
<label id="PanelUI-panic-actionlist-cookies" class="PanelUI-panic-actionlist">&panicButton.view.deleteCookies;</label>
<label id="PanelUI-panic-actionlist-history" class="PanelUI-panic-actionlist">&panicButton.view.deleteHistory;</label>
<label id="PanelUI-panic-actionlist-newwindow" class="PanelUI-panic-actionlist">&panicButton.view.openNewWindow;</label>
<label id="PanelUI-panic-warning">&panicButton.view.undoWarning;</label>
</vbox>
<button id="PanelUI-panic-view-button"
label="&panicButton.view.forgetButton;"/>
</vbox>
</panelview>
</panelmultiview>
<!-- These menupopups are located here to prevent flickering,
see bug 492960 comment 20. -->
<menupopup id="customizationPanelItemContextMenu">
<menuitem oncommand="gCustomizeMode.addToToolbar(document.popupNode)"
closemenu="single"
class="customize-context-moveToToolbar"
accesskey="&customizeMenu.moveToToolbar.accesskey;"
label="&customizeMenu.moveToToolbar.label;"/>
<menuitem oncommand="gCustomizeMode.removeFromArea(document.popupNode)"
closemenu="single"
class="customize-context-removeFromPanel"
accesskey="&customizeMenu.removeFromMenu.accesskey;"
label="&customizeMenu.removeFromMenu.label;"/>
<menuseparator/>
<menuitem command="cmd_CustomizeToolbars"
class="viewCustomizeToolbar"
accesskey="&viewCustomizeToolbar.accesskey;"
label="&viewCustomizeToolbar.label;"/>
</menupopup>
<menupopup id="customizationPaletteItemContextMenu">
<menuitem oncommand="gCustomizeMode.addToToolbar(document.popupNode)"
class="customize-context-addToToolbar"
accesskey="&customizeMenu.addToToolbar.accesskey;"
label="&customizeMenu.addToToolbar.label;"/>
<menuitem oncommand="gCustomizeMode.addToPanel(document.popupNode)"
class="customize-context-addToPanel"
accesskey="&customizeMenu.addToPanel.accesskey;"
label="&customizeMenu.addToPanel.label;"/>
</menupopup>
<menupopup id="customizationPanelContextMenu">
<menuitem command="cmd_CustomizeToolbars"
accesskey="&customizeMenu.addMoreItems.accesskey;"
label="&customizeMenu.addMoreItems.label;"/>
</menupopup>
</panel>
<panel id="widget-overflow"
role="group"
type="arrow"
noautofocus="true"
context="toolbar-context-menu"
position="bottomcenter topright"
hidden="true">
<vbox id="widget-overflow-scroller">
<vbox id="widget-overflow-list" class="widget-overflow-list"
overflowfortoolbar="nav-bar"/>
</vbox>
</panel>
<panel id="panic-button-success-notification"
type="arrow"
position="bottomcenter topright"
hidden="true"
role="alert"
orient="vertical">
<hbox id="panic-button-success-header">
<image id="panic-button-success-icon" alt=""/>
<vbox>
<description>&panicButton.thankyou.msg1;</description>
<description>&panicButton.thankyou.msg2;</description>
</vbox>
</hbox>
<button label="&panicButton.thankyou.buttonlabel;"
id="panic-button-success-closebutton"
oncommand="PanicButtonNotifier.close()"/>
</panel>

View file

@ -0,0 +1,558 @@
/* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
* You can obtain one at http://mozilla.org/MPL/2.0/. */
XPCOMUtils.defineLazyModuleGetter(this, "CustomizableUI",
"resource:///modules/CustomizableUI.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ScrollbarSampler",
"resource:///modules/ScrollbarSampler.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "ShortcutUtils",
"resource://gre/modules/ShortcutUtils.jsm");
XPCOMUtils.defineLazyModuleGetter(this, "AppConstants",
"resource://gre/modules/AppConstants.jsm");
/**
* Maintains the state and dispatches events for the main menu panel.
*/
const PanelUI = {
/** Panel events that we listen for. **/
get kEvents() {
return ["popupshowing", "popupshown", "popuphiding", "popuphidden"];
},
/**
* Used for lazily getting and memoizing elements from the document. Lazy
* getters are set in init, and memoizing happens after the first retrieval.
*/
get kElements() {
return {
contents: "PanelUI-contents",
mainView: "PanelUI-mainView",
multiView: "PanelUI-multiView",
helpView: "PanelUI-helpView",
menuButton: "PanelUI-menu-button",
panel: "PanelUI-popup",
scroller: "PanelUI-contents-scroller"
};
},
_initialized: false,
init: function() {
for (let [k, v] of Object.entries(this.kElements)) {
// Need to do fresh let-bindings per iteration
let getKey = k;
let id = v;
this.__defineGetter__(getKey, function() {
delete this[getKey];
return this[getKey] = document.getElementById(id);
});
}
this.menuButton.addEventListener("mousedown", this);
this.menuButton.addEventListener("keypress", this);
this._overlayScrollListenerBoundFn = this._overlayScrollListener.bind(this);
window.matchMedia("(-moz-overlay-scrollbars)").addListener(this._overlayScrollListenerBoundFn);
CustomizableUI.addListener(this);
this._initialized = true;
},
_eventListenersAdded: false,
_ensureEventListenersAdded: function() {
if (this._eventListenersAdded)
return;
this._addEventListeners();
},
_addEventListeners: function() {
for (let event of this.kEvents) {
this.panel.addEventListener(event, this);
}
this.helpView.addEventListener("ViewShowing", this._onHelpViewShow, false);
this._eventListenersAdded = true;
},
uninit: function() {
for (let event of this.kEvents) {
this.panel.removeEventListener(event, this);
}
this.helpView.removeEventListener("ViewShowing", this._onHelpViewShow);
this.menuButton.removeEventListener("mousedown", this);
this.menuButton.removeEventListener("keypress", this);
window.matchMedia("(-moz-overlay-scrollbars)").removeListener(this._overlayScrollListenerBoundFn);
CustomizableUI.removeListener(this);
this._overlayScrollListenerBoundFn = null;
},
/**
* Customize mode extracts the mainView and puts it somewhere else while the
* user customizes. Upon completion, this function can be called to put the
* panel back to where it belongs in normal browsing mode.
*
* @param aMainView
* The mainView node to put back into place.
*/
setMainView: function(aMainView) {
this._ensureEventListenersAdded();
this.multiView.setMainView(aMainView);
},
/**
* Opens the menu panel if it's closed, or closes it if it's
* open.
*
* @param aEvent the event that triggers the toggle.
*/
toggle: function(aEvent) {
// Don't show the panel if the window is in customization mode,
// since this button doubles as an exit path for the user in this case.
if (document.documentElement.hasAttribute("customizing")) {
return;
}
this._ensureEventListenersAdded();
if (this.panel.state == "open") {
this.hide();
} else if (this.panel.state == "closed") {
this.show(aEvent);
}
},
/**
* Opens the menu panel. If the event target has a child with the
* toolbarbutton-icon attribute, the panel will be anchored on that child.
* Otherwise, the panel is anchored on the event target itself.
*
* @param aEvent the event (if any) that triggers showing the menu.
*/
show: function(aEvent) {
return new Promise(resolve => {
this.ensureReady().then(() => {
if (this.panel.state == "open" ||
document.documentElement.hasAttribute("customizing")) {
resolve();
return;
}
let editControlPlacement = CustomizableUI.getPlacementOfWidget("edit-controls");
if (editControlPlacement && editControlPlacement.area == CustomizableUI.AREA_PANEL) {
updateEditUIVisibility();
}
let personalBookmarksPlacement = CustomizableUI.getPlacementOfWidget("personal-bookmarks");
if (personalBookmarksPlacement &&
personalBookmarksPlacement.area == CustomizableUI.AREA_PANEL) {
PlacesToolbarHelper.customizeChange();
}
let anchor;
if (!aEvent ||
aEvent.type == "command") {
anchor = this.menuButton;
} else {
anchor = aEvent.target;
}
this.panel.addEventListener("popupshown", function onPopupShown() {
this.removeEventListener("popupshown", onPopupShown);
resolve();
});
let iconAnchor =
document.getAnonymousElementByAttribute(anchor, "class",
"toolbarbutton-icon");
this.panel.openPopup(iconAnchor || anchor);
}, (reason) => {
console.error("Error showing the PanelUI menu", reason);
});
});
},
/**
* If the menu panel is being shown, hide it.
*/
hide: function() {
if (document.documentElement.hasAttribute("customizing")) {
return;
}
this.panel.hidePopup();
},
handleEvent: function(aEvent) {
// Ignore context menus and menu button menus showing and hiding:
if (aEvent.type.startsWith("popup") &&
aEvent.target != this.panel) {
return;
}
switch (aEvent.type) {
case "popupshowing":
this._adjustLabelsForAutoHyphens();
// Fall through
case "popupshown":
// Fall through
case "popuphiding":
// Fall through
case "popuphidden":
this._updatePanelButton(aEvent.target);
break;
case "mousedown":
if (aEvent.button == 0)
this.toggle(aEvent);
break;
case "keypress":
this.toggle(aEvent);
break;
}
},
get isReady() {
return !!this._isReady;
},
/**
* Registering the menu panel is done lazily for performance reasons. This
* method is exposed so that CustomizationMode can force panel-readyness in the
* event that customization mode is started before the panel has been opened
* by the user.
*
* @param aCustomizing (optional) set to true if this was called while entering
* customization mode. If that's the case, we trust that customization
* mode will handle calling beginBatchUpdate and endBatchUpdate.
*
* @return a Promise that resolves once the panel is ready to roll.
*/
ensureReady: function(aCustomizing=false) {
if (this._readyPromise) {
return this._readyPromise;
}
this._readyPromise = Task.spawn(function*() {
if (!this._initialized) {
yield new Promise(resolve => {
let delayedStartupObserver = (aSubject, aTopic, aData) => {
if (aSubject == window) {
Services.obs.removeObserver(delayedStartupObserver, "browser-delayed-startup-finished");
resolve();
}
};
Services.obs.addObserver(delayedStartupObserver, "browser-delayed-startup-finished", false);
});
}
this.contents.setAttributeNS("http://www.w3.org/XML/1998/namespace", "lang",
getLocale());
if (!this._scrollWidth) {
// In order to properly center the contents of the panel, while ensuring
// that we have enough space on either side to show a scrollbar, we have to
// do a bit of hackery. In particular, we calculate a new width for the
// scroller, based on the system scrollbar width.
this._scrollWidth =
(yield ScrollbarSampler.getSystemScrollbarWidth()) + "px";
let cstyle = window.getComputedStyle(this.scroller);
let widthStr = cstyle.width;
// Get the calculated padding on the left and right sides of
// the scroller too. We'll use that in our final calculation so
// that if a scrollbar appears, we don't have the contents right
// up against the edge of the scroller.
let paddingLeft = cstyle.paddingLeft;
let paddingRight = cstyle.paddingRight;
let calcStr = [widthStr, this._scrollWidth,
paddingLeft, paddingRight].join(" + ");
this.scroller.style.width = "calc(" + calcStr + ")";
}
if (aCustomizing) {
CustomizableUI.registerMenuPanel(this.contents);
} else {
this.beginBatchUpdate();
try {
CustomizableUI.registerMenuPanel(this.contents);
} finally {
this.endBatchUpdate();
}
}
this._updateQuitTooltip();
this.panel.hidden = false;
this._isReady = true;
}.bind(this)).then(null, Cu.reportError);
return this._readyPromise;
},
/**
* Switch the panel to the main view if it's not already
* in that view.
*/
showMainView: function() {
this._ensureEventListenersAdded();
this.multiView.showMainView();
},
/**
* Switch the panel to the help view if it's not already
* in that view.
*/
showHelpView: function(aAnchor) {
this._ensureEventListenersAdded();
this.multiView.showSubView("PanelUI-helpView", aAnchor);
},
/**
* Shows a subview in the panel with a given ID.
*
* @param aViewId the ID of the subview to show.
* @param aAnchor the element that spawned the subview.
* @param aPlacementArea the CustomizableUI area that aAnchor is in.
*/
showSubView: Task.async(function*(aViewId, aAnchor, aPlacementArea) {
this._ensureEventListenersAdded();
let viewNode = document.getElementById(aViewId);
if (!viewNode) {
Cu.reportError("Could not show panel subview with id: " + aViewId);
return;
}
if (!aAnchor) {
Cu.reportError("Expected an anchor when opening subview with id: " + aViewId);
return;
}
if (aPlacementArea == CustomizableUI.AREA_PANEL) {
this.multiView.showSubView(aViewId, aAnchor);
} else if (!aAnchor.open) {
aAnchor.open = true;
let tempPanel = document.createElement("panel");
tempPanel.setAttribute("type", "arrow");
tempPanel.setAttribute("id", "customizationui-widget-panel");
tempPanel.setAttribute("class", "cui-widget-panel");
tempPanel.setAttribute("viewId", aViewId);
if (aAnchor.getAttribute("tabspecific")) {
tempPanel.setAttribute("tabspecific", true);
}
if (this._disableAnimations) {
tempPanel.setAttribute("animate", "false");
}
tempPanel.setAttribute("context", "");
document.getElementById(CustomizableUI.AREA_NAVBAR).appendChild(tempPanel);
// If the view has a footer, set a convenience class on the panel.
tempPanel.classList.toggle("cui-widget-panelWithFooter",
viewNode.querySelector(".panel-subview-footer"));
let multiView = document.createElement("panelmultiview");
multiView.setAttribute("id", "customizationui-widget-multiview");
multiView.setAttribute("nosubviews", "true");
tempPanel.appendChild(multiView);
multiView.setAttribute("mainViewIsSubView", "true");
multiView.setMainView(viewNode);
viewNode.classList.add("cui-widget-panelview");
let viewShown = false;
let panelRemover = () => {
viewNode.classList.remove("cui-widget-panelview");
if (viewShown) {
CustomizableUI.removePanelCloseListeners(tempPanel);
tempPanel.removeEventListener("popuphidden", panelRemover);
let evt = new CustomEvent("ViewHiding", {detail: viewNode});
viewNode.dispatchEvent(evt);
}
aAnchor.open = false;
this.multiView.appendChild(viewNode);
tempPanel.remove();
};
// Emit the ViewShowing event so that the widget definition has a chance
// to lazily populate the subview with things.
let detail = {
blockers: new Set(),
addBlocker(aPromise) {
this.blockers.add(aPromise);
},
};
let evt = new CustomEvent("ViewShowing", { bubbles: true, cancelable: true, detail });
viewNode.dispatchEvent(evt);
let cancel = evt.defaultPrevented;
if (detail.blockers.size) {
try {
let results = yield Promise.all(detail.blockers);
cancel = cancel || results.some(val => val === false);
} catch (e) {
Components.utils.reportError(e);
cancel = true;
}
}
if (cancel) {
panelRemover();
return;
}
viewShown = true;
CustomizableUI.addPanelCloseListeners(tempPanel);
tempPanel.addEventListener("popuphidden", panelRemover);
let iconAnchor =
document.getAnonymousElementByAttribute(aAnchor, "class",
"toolbarbutton-icon");
if (iconAnchor && aAnchor.id) {
iconAnchor.setAttribute("consumeanchor", aAnchor.id);
}
tempPanel.openPopup(iconAnchor || aAnchor, "bottomcenter topright");
}
}),
/**
* NB: The enable- and disableSingleSubviewPanelAnimations methods only
* affect the hiding/showing animations of single-subview panels (tempPanel
* in the showSubView method).
*/
disableSingleSubviewPanelAnimations: function() {
this._disableAnimations = true;
},
enableSingleSubviewPanelAnimations: function() {
this._disableAnimations = false;
},
onWidgetAfterDOMChange: function(aNode, aNextNode, aContainer, aWasRemoval) {
if (aContainer != this.contents) {
return;
}
if (aWasRemoval) {
aNode.removeAttribute("auto-hyphens");
}
},
onWidgetBeforeDOMChange: function(aNode, aNextNode, aContainer, aIsRemoval) {
if (aContainer != this.contents) {
return;
}
if (!aIsRemoval &&
(this.panel.state == "open" ||
document.documentElement.hasAttribute("customizing"))) {
this._adjustLabelsForAutoHyphens(aNode);
}
},
/**
* Signal that we're about to make a lot of changes to the contents of the
* panels all at once. For performance, we ignore the mutations.
*/
beginBatchUpdate: function() {
this._ensureEventListenersAdded();
this.multiView.ignoreMutations = true;
},
/**
* Signal that we're done making bulk changes to the panel. We now pay
* attention to mutations. This automatically synchronizes the multiview
* container with whichever view is displayed if the panel is open.
*/
endBatchUpdate: function(aReason) {
this._ensureEventListenersAdded();
this.multiView.ignoreMutations = false;
},
_adjustLabelsForAutoHyphens: function(aNode) {
let toolbarButtons = aNode ? [aNode] :
this.contents.querySelectorAll(".toolbarbutton-1");
for (let node of toolbarButtons) {
let label = node.getAttribute("label");
if (!label) {
continue;
}
if (label.includes("\u00ad")) {
node.setAttribute("auto-hyphens", "off");
} else {
node.removeAttribute("auto-hyphens");
}
}
},
/**
* Sets the anchor node into the open or closed state, depending
* on the state of the panel.
*/
_updatePanelButton: function() {
this.menuButton.open = this.panel.state == "open" ||
this.panel.state == "showing";
},
_onHelpViewShow: function(aEvent) {
// Call global menu setup function
buildHelpMenu();
let helpMenu = document.getElementById("menu_HelpPopup");
let items = this.getElementsByTagName("vbox")[0];
let attrs = ["oncommand", "onclick", "label", "key", "disabled"];
let NSXUL = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
// Remove all buttons from the view
while (items.firstChild) {
items.removeChild(items.firstChild);
}
// Add the current set of menuitems of the Help menu to this view
let menuItems = Array.prototype.slice.call(helpMenu.getElementsByTagName("menuitem"));
let fragment = document.createDocumentFragment();
for (let node of menuItems) {
if (node.hidden)
continue;
let button = document.createElementNS(NSXUL, "toolbarbutton");
// Copy specific attributes from a menuitem of the Help menu
for (let attrName of attrs) {
if (!node.hasAttribute(attrName))
continue;
button.setAttribute(attrName, node.getAttribute(attrName));
}
button.setAttribute("class", "subviewbutton");
fragment.appendChild(button);
}
items.appendChild(fragment);
},
_updateQuitTooltip: function() {
if (AppConstants.platform == "win") {
return;
}
let tooltipId = AppConstants.platform == "macosx" ?
"quit-button.tooltiptext.mac" :
"quit-button.tooltiptext.linux2";
let brands = Services.strings.createBundle("chrome://branding/locale/brand.properties");
let stringArgs = [brands.GetStringFromName("brandShortName")];
let key = document.getElementById("key_quitApplication");
stringArgs.push(ShortcutUtils.prettifyShortcut(key));
let tooltipString = CustomizableUI.getLocalizedProperty({x: tooltipId}, "x", stringArgs);
let quitButton = document.getElementById("PanelUI-quit");
quitButton.setAttribute("tooltiptext", tooltipString);
},
_overlayScrollListenerBoundFn: null,
_overlayScrollListener: function(aMQL) {
ScrollbarSampler.resetSystemScrollbarWidth();
this._scrollWidth = null;
},
};
XPCOMUtils.defineConstant(this, "PanelUI", PanelUI);
/**
* Gets the currently selected locale for display.
* @return the selected locale or "en-US" if none is selected
*/
function getLocale() {
try {
let chromeRegistry = Cc["@mozilla.org/chrome/chrome-registry;1"]
.getService(Ci.nsIXULChromeRegistry);
return chromeRegistry.getSelectedLocale("browser");
} catch (ex) {
return "en-US";
}
}

View file

@ -0,0 +1,509 @@
<?xml version="1.0"?>
<!-- This Source Code Form is subject to the terms of the Mozilla Public
- License, v. 2.0. If a copy of the MPL was not distributed with this
- file, You can obtain one at http://mozilla.org/MPL/2.0/. -->
<bindings id="browserPanelUIBindings"
xmlns="http://www.mozilla.org/xbl"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul"
xmlns:xbl="http://www.mozilla.org/xbl">
<binding id="panelmultiview">
<resources>
<stylesheet src="chrome://browser/content/customizableui/panelUI.css"/>
</resources>
<content>
<xul:box anonid="viewContainer" class="panel-viewcontainer" xbl:inherits="panelopen,viewtype,transitioning">
<xul:stack anonid="viewStack" xbl:inherits="viewtype,transitioning" viewtype="main" class="panel-viewstack">
<xul:vbox anonid="mainViewContainer" class="panel-mainview" xbl:inherits="viewtype"/>
<!-- Used to capture click events over the PanelUI-mainView if we're in
subview mode. That way, any click on the PanelUI-mainView causes us
to revert to the mainView mode, whereupon PanelUI-click-capture then
allows click events to go through it. -->
<xul:vbox anonid="clickCapturer" class="panel-clickcapturer"/>
<!-- We manually set display: none (via a CSS attribute selector) on the
subviews that are not being displayed. We're using this over a deck
because a deck assumes the size of its largest child, regardless of
whether or not it is shown. That's not good for our case, since we
want to allow each subview to be uniquely sized. -->
<xul:vbox anonid="subViews" class="panel-subviews" xbl:inherits="panelopen">
<children includes="panelview"/>
</xul:vbox>
</xul:stack>
</xul:box>
</content>
<implementation implements="nsIDOMEventListener">
<field name="_clickCapturer" readonly="true">
document.getAnonymousElementByAttribute(this, "anonid", "clickCapturer");
</field>
<field name="_viewContainer" readonly="true">
document.getAnonymousElementByAttribute(this, "anonid", "viewContainer");
</field>
<field name="_mainViewContainer" readonly="true">
document.getAnonymousElementByAttribute(this, "anonid", "mainViewContainer");
</field>
<field name="_subViews" readonly="true">
document.getAnonymousElementByAttribute(this, "anonid", "subViews");
</field>
<field name="_viewStack" readonly="true">
document.getAnonymousElementByAttribute(this, "anonid", "viewStack");
</field>
<field name="_panel" readonly="true">
this.parentNode;
</field>
<field name="_currentSubView">null</field>
<field name="_anchorElement">null</field>
<field name="_mainViewHeight">0</field>
<field name="_subViewObserver">null</field>
<field name="__transitioning">false</field>
<field name="_ignoreMutations">false</field>
<property name="showingSubView" readonly="true"
onget="return this._viewStack.getAttribute('viewtype') == 'subview'"/>
<property name="_mainViewId" onget="return this.getAttribute('mainViewId');" onset="this.setAttribute('mainViewId', val); return val;"/>
<property name="_mainView" readonly="true"
onget="return this._mainViewId ? document.getElementById(this._mainViewId) : null;"/>
<property name="showingSubViewAsMainView" readonly="true"
onget="return this.getAttribute('mainViewIsSubView') == 'true'"/>
<property name="ignoreMutations">
<getter>
return this._ignoreMutations;
</getter>
<setter><![CDATA[
this._ignoreMutations = val;
if (!val && this._panel.state == "open") {
if (this.showingSubView) {
this._syncContainerWithSubView();
} else {
this._syncContainerWithMainView();
}
}
]]></setter>
</property>
<property name="_transitioning">
<getter>
return this.__transitioning;
</getter>
<setter><![CDATA[
this.__transitioning = val;
if (val) {
this.setAttribute("transitioning", "true");
} else {
this.removeAttribute("transitioning");
}
]]></setter>
</property>
<constructor><![CDATA[
this._clickCapturer.addEventListener("click", this);
this._panel.addEventListener("popupshowing", this);
this._panel.addEventListener("popupshown", this);
this._panel.addEventListener("popuphidden", this);
this._subViews.addEventListener("overflow", this);
this._mainViewContainer.addEventListener("overflow", this);
// Get a MutationObserver ready to react to subview size changes. We
// only attach this MutationObserver when a subview is being displayed.
this._subViewObserver =
new MutationObserver(this._syncContainerWithSubView.bind(this));
this._mainViewObserver =
new MutationObserver(this._syncContainerWithMainView.bind(this));
this._mainViewContainer.setAttribute("panelid",
this._panel.id);
if (this._mainView) {
this.setMainView(this._mainView);
}
this.setAttribute("viewtype", "main");
]]></constructor>
<destructor><![CDATA[
if (this._mainView) {
this._mainView.removeAttribute("mainview");
}
this._mainViewObserver.disconnect();
this._subViewObserver.disconnect();
this._panel.removeEventListener("popupshowing", this);
this._panel.removeEventListener("popupshown", this);
this._panel.removeEventListener("popuphidden", this);
this._subViews.removeEventListener("overflow", this);
this._mainViewContainer.removeEventListener("overflow", this);
this._clickCapturer.removeEventListener("click", this);
]]></destructor>
<method name="setMainView">
<parameter name="aNewMainView"/>
<body><![CDATA[
if (this._mainView) {
this._mainViewObserver.disconnect();
this._subViews.appendChild(this._mainView);
this._mainView.removeAttribute("mainview");
}
this._mainViewId = aNewMainView.id;
aNewMainView.setAttribute("mainview", "true");
this._mainViewContainer.appendChild(aNewMainView);
]]></body>
</method>
<method name="showMainView">
<body><![CDATA[
if (this.showingSubView) {
let viewNode = this._currentSubView;
let evt = document.createEvent("CustomEvent");
evt.initCustomEvent("ViewHiding", true, true, viewNode);
viewNode.dispatchEvent(evt);
viewNode.removeAttribute("current");
this._currentSubView = null;
this._subViewObserver.disconnect();
this._setViewContainerHeight(this._mainViewHeight);
this.setAttribute("viewtype", "main");
}
this._shiftMainView();
]]></body>
</method>
<method name="showSubView">
<parameter name="aViewId"/>
<parameter name="aAnchor"/>
<body><![CDATA[
Task.spawn(function*() {
let viewNode = this.querySelector("#" + aViewId);
viewNode.setAttribute("current", true);
// Emit the ViewShowing event so that the widget definition has a chance
// to lazily populate the subview with things.
let detail = {
blockers: new Set(),
addBlocker(aPromise) {
this.blockers.add(aPromise);
},
};
let evt = new CustomEvent("ViewShowing", { bubbles: true, cancelable: true, detail });
viewNode.dispatchEvent(evt);
let cancel = evt.defaultPrevented;
if (detail.blockers.size) {
try {
let results = yield Promise.all(detail.blockers);
cancel = cancel || results.some(val => val === false);
} catch (e) {
Components.utils.reportError(e);
cancel = true;
}
}
if (cancel) {
return;
}
this._currentSubView = viewNode;
// Now we have to transition the panel. There are a few parts to this:
//
// 1) The main view content gets shifted so that the center of the anchor
// node is at the left-most edge of the panel.
// 2) The subview deck slides in so that it takes up almost all of the
// panel.
// 3) If the subview is taller then the main panel contents, then the panel
// must grow to meet that new height. Otherwise, it must shrink.
//
// All three of these actions make use of CSS transformations, so they
// should all occur simultaneously.
this.setAttribute("viewtype", "subview");
this._shiftMainView(aAnchor);
this._mainViewHeight = this._viewStack.clientHeight;
let newHeight = this._heightOfSubview(viewNode, this._subViews);
this._setViewContainerHeight(newHeight);
this._subViewObserver.observe(viewNode, {
attributes: true,
characterData: true,
childList: true,
subtree: true
});
}.bind(this));
]]></body>
</method>
<method name="_setViewContainerHeight">
<parameter name="aHeight"/>
<body><![CDATA[
let container = this._viewContainer;
this._transitioning = true;
let onTransitionEnd = () => {
container.removeEventListener("transitionend", onTransitionEnd);
this._transitioning = false;
};
container.addEventListener("transitionend", onTransitionEnd);
container.style.height = `${aHeight}px`;
]]></body>
</method>
<method name="_shiftMainView">
<parameter name="aAnchor"/>
<body><![CDATA[
if (aAnchor) {
// We need to find the edge of the anchor, relative to the main panel.
// Then we need to add half the width of the anchor. This is the target
// that we need to transition to.
let anchorRect = aAnchor.getBoundingClientRect();
let mainViewRect = this._mainViewContainer.getBoundingClientRect();
let center = aAnchor.clientWidth / 2;
let direction = aAnchor.ownerDocument.defaultView.getComputedStyle(aAnchor, null).direction;
let edge;
if (direction == "ltr") {
edge = anchorRect.left - mainViewRect.left;
} else {
edge = mainViewRect.right - anchorRect.right;
}
// If the anchor is an element on the far end of the mainView we
// don't want to shift the mainView too far, we would reveal empty
// space otherwise.
let cstyle = window.getComputedStyle(document.documentElement, null);
let exitSubViewGutterWidth =
cstyle.getPropertyValue("--panel-ui-exit-subview-gutter-width");
let maxShift = mainViewRect.width - parseInt(exitSubViewGutterWidth);
let target = Math.min(maxShift, edge + center);
let neg = direction == "ltr" ? "-" : "";
this._mainViewContainer.style.transform = `translateX(${neg}${target}px)`;
aAnchor.setAttribute("panel-multiview-anchor", true);
} else {
this._mainViewContainer.style.transform = "";
if (this.anchorElement)
this.anchorElement.removeAttribute("panel-multiview-anchor");
}
this.anchorElement = aAnchor;
]]></body>
</method>
<method name="handleEvent">
<parameter name="aEvent"/>
<body><![CDATA[
if (aEvent.type.startsWith("popup") && aEvent.target != this._panel) {
// Shouldn't act on e.g. context menus being shown from within the panel.
return;
}
switch (aEvent.type) {
case "click":
if (aEvent.originalTarget == this._clickCapturer) {
this.showMainView();
}
break;
case "overflow":
if (aEvent.target.localName == "vbox") {
// Resize the right view on the next tick.
if (this.showingSubView) {
setTimeout(this._syncContainerWithSubView.bind(this), 0);
} else if (!this.transitioning) {
setTimeout(this._syncContainerWithMainView.bind(this), 0);
}
}
break;
case "popupshowing":
this.setAttribute("panelopen", "true");
// Bug 941196 - The panel can get taller when opening a subview. Disabling
// autoPositioning means that the panel won't jump around if an opened
// subview causes the panel to exceed the dimensions of the screen in the
// direction that the panel originally opened in. This property resets
// every time the popup closes, which is why we have to set it each time.
this._panel.autoPosition = false;
this._syncContainerWithMainView();
this._mainViewObserver.observe(this._mainView, {
attributes: true,
characterData: true,
childList: true,
subtree: true
});
break;
case "popupshown":
this._setMaxHeight();
break;
case "popuphidden":
this.removeAttribute("panelopen");
this._mainView.style.removeProperty("height");
this.showMainView();
this._mainViewObserver.disconnect();
break;
}
]]></body>
</method>
<method name="_shouldSetPosition">
<body><![CDATA[
return this.getAttribute("nosubviews") == "true";
]]></body>
</method>
<method name="_shouldSetHeight">
<body><![CDATA[
return this.getAttribute("nosubviews") != "true";
]]></body>
</method>
<method name="_setMaxHeight">
<body><![CDATA[
if (!this._shouldSetHeight())
return;
// Ignore the mutation that'll fire when we set the height of
// the main view.
this.ignoreMutations = true;
this._mainView.style.height =
this.getBoundingClientRect().height + "px";
this.ignoreMutations = false;
]]></body>
</method>
<method name="_adjustContainerHeight">
<body><![CDATA[
if (!this.ignoreMutations && !this.showingSubView && !this._transitioning) {
let height;
if (this.showingSubViewAsMainView) {
height = this._heightOfSubview(this._mainView);
} else {
height = this._mainView.scrollHeight;
}
this._viewContainer.style.height = height + "px";
}
]]></body>
</method>
<method name="_syncContainerWithSubView">
<body><![CDATA[
// Check that this panel is still alive:
if (!this._panel || !this._panel.parentNode) {
return;
}
if (!this.ignoreMutations && this.showingSubView) {
let newHeight = this._heightOfSubview(this._currentSubView, this._subViews);
this._viewContainer.style.height = newHeight + "px";
}
]]></body>
</method>
<method name="_syncContainerWithMainView">
<body><![CDATA[
// Check that this panel is still alive:
if (!this._panel || !this._panel.parentNode) {
return;
}
if (this._shouldSetPosition()) {
this._panel.adjustArrowPosition();
}
if (this._shouldSetHeight()) {
this._adjustContainerHeight();
}
]]></body>
</method>
<!-- Call this when the height of one of your views (the main view or a
subview) changes and you want the heights of the multiview and panel
to be the same as the view's height.
If the caller can give a hint of the expected height change with the
optional aExpectedChange parameter, it prevents flicker. -->
<method name="setHeightToFit">
<parameter name="aExpectedChange"/>
<body><![CDATA[
// Set the max-height to zero, wait until the height is actually
// updated, and then remove it. If it's not removed, weird things can
// happen, like widgets in the panel won't respond to clicks even
// though they're visible.
let count = 5;
let height = getComputedStyle(this).height;
if (aExpectedChange)
this.style.maxHeight = (parseInt(height) + aExpectedChange) + "px";
else
this.style.maxHeight = "0";
let interval = setInterval(() => {
if (height != getComputedStyle(this).height || --count == 0) {
clearInterval(interval);
this.style.removeProperty("max-height");
}
}, 0);
]]></body>
</method>
<method name="_heightOfSubview">
<parameter name="aSubview"/>
<parameter name="aContainerToCheck"/>
<body><![CDATA[
function getFullHeight(element) {
// XXXgijs: unfortunately, scrollHeight rounds values, and there's no alternative
// that works with overflow: auto elements. Fortunately for us,
// we have exactly 1 (potentially) scrolling element in here (the subview body),
// and rounding 1 value is OK - rounding more than 1 and adding them means we get
// off-by-1 errors. Now we might be off by a subpixel, but we care less about that.
// So, use scrollHeight *only* if the element is vertically scrollable.
let height;
let elementCS;
if (element.scrollTopMax) {
height = element.scrollHeight;
// Bounding client rects include borders, scrollHeight doesn't:
elementCS = win.getComputedStyle(element);
height += parseFloat(elementCS.borderTopWidth) +
parseFloat(elementCS.borderBottomWidth);
} else {
height = element.getBoundingClientRect().height;
if (height > 0) {
elementCS = win.getComputedStyle(element);
}
}
if (elementCS) {
// Include margins - but not borders or paddings because they
// were dealt with above.
height += parseFloat(elementCS.marginTop) + parseFloat(elementCS.marginBottom);
}
return height;
}
let win = aSubview.ownerDocument.defaultView;
let body = aSubview.querySelector(".panel-subview-body");
let height = getFullHeight(body || aSubview);
if (body) {
let header = aSubview.querySelector(".panel-subview-header");
let footer = aSubview.querySelector(".panel-subview-footer");
height += (header ? getFullHeight(header) : 0) +
(footer ? getFullHeight(footer) : 0);
}
if (aContainerToCheck) {
let containerCS = win.getComputedStyle(aContainerToCheck);
height += parseFloat(containerCS.paddingTop) + parseFloat(containerCS.paddingBottom);
}
return Math.ceil(height);
]]></body>
</method>
</implementation>
</binding>
<binding id="panelview">
<implementation>
<property name="panelMultiView" readonly="true">
<getter><![CDATA[
if (this.parentNode.localName != "panelmultiview") {
return document.getBindingParent(this.parentNode);
}
return this.parentNode;
]]></getter>
</property>
</implementation>
</binding>
</bindings>

View file

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

View file

@ -0,0 +1,21 @@
# -*- Mode: python; indent-tabs-mode: nil; tab-width: 40 -*-
# vim: set filetype=python:
# This Source Code Form is subject to the terms of the Mozilla Public
# License, v. 2.0. If a copy of the MPL was not distributed with this
# file, You can obtain one at http://mozilla.org/MPL/2.0/.
DIRS += [
'content',
]
EXTRA_JS_MODULES += [
'CustomizableUI.jsm',
'CustomizableWidgets.jsm',
'CustomizeMode.jsm',
'DragPositionManager.jsm',
'PanelWideWidgetTracker.jsm',
'ScrollbarSampler.jsm',
]
if CONFIG['MOZ_WIDGET_TOOLKIT'] in ('windows', 'cocoa'):
DEFINES['CAN_DRAW_IN_TITLEBAR'] = 1