Bug 941409, add support for an anchor attribute to menus so that the popup can be anchored to a different element than the menu itself, this is used to avoid repositioning the bookmarks toolbar button menu, also changes popups to default to vertical orientation [Australis] r=neil,mconley

This commit is contained in:
Neil Deakin 2014-01-28 11:28:45 -05:00
parent baa42a3670
commit 9cab3a8fc4
13 changed files with 143 additions and 57 deletions

View File

@ -708,6 +708,7 @@
type="menu-button"
label="&bookmarksMenuButton.label;"
tooltiptext="&bookmarksMenuButton.tooltip;"
anchor="dropmarker"
ondragenter="PlacesMenuDNDHandler.onDragEnter(event);"
ondragover="PlacesMenuDNDHandler.onDragOver(event);"
ondragleave="PlacesMenuDNDHandler.onDragLeave(event);"
@ -718,7 +719,6 @@
placespopup="true"
context="placesContext"
openInTabs="children"
anonanchorclass="toolbarbutton-menubutton-dropmarker"
oncommand="BookmarksEventHandler.onCommand(event, this.parentNode._placesView);"
onclick="BookmarksEventHandler.onClick(event, this.parentNode._placesView);"
onpopupshowing="BookmarkingUI.onPopupShowing(event);

View File

@ -490,7 +490,7 @@
<binding id="places-popup-arrow"
extends="chrome://browser/content/places/menu.xml#places-popup-base">
<content flip="both" side="top" position="bottomcenter topleft">
<xul:box anonid="container" class="panel-arrowcontainer" flex="1"
<xul:vbox anonid="container" class="panel-arrowcontainer" flex="1"
xbl:inherits="side,panelopen">
<xul:box anonid="arrowbox" class="panel-arrowbox">
<xul:image anonid="arrow" class="panel-arrow" xbl:inherits="side"/>
@ -504,7 +504,7 @@
<children/>
</xul:arrowscrollbox>
</xul:box>
</xul:box>
</xul:vbox>
</content>
<implementation>
@ -521,20 +521,19 @@
var container = document.getAnonymousElementByAttribute(this, "anonid", "container");
var arrowbox = document.getAnonymousElementByAttribute(this, "anonid", "arrowbox");
// if this panel has a "sliding" arrow, we may have previously set margins...
arrowbox.style.removeProperty("margin");
var position = this.alignmentPosition;
var offset = this.alignmentOffset;
// if this panel has a "sliding" arrow, we may have previously set margins...
arrowbox.style.removeProperty("transform");
if (position.indexOf("start_") == 0 || position.indexOf("end_") == 0) {
container.orient = "";
container.orient = "horizontal";
arrowbox.orient = "vertical";
if (position.indexOf("_after") > 0) {
arrowbox.pack = "end";
arrowbox.style.marginBottom = this.alignmentOffset + "px";
} else {
arrowbox.pack = "start";
arrowbox.style.marginTop = this.alignmentOffset + "px";
}
arrowbox.style.transform = "translate(0, " + -offset + "px)";
// The assigned side stays the same regardless of direction.
var isRTL = (window.getComputedStyle(this).direction == "rtl");
@ -549,15 +548,14 @@
}
}
else if (position.indexOf("before_") == 0 || position.indexOf("after_") == 0) {
container.orient = "vertical";
container.orient = "";
arrowbox.orient = "";
if (position.indexOf("_end") > 0) {
arrowbox.pack = "end";
arrowbox.style.marginRight = this.alignmentOffset + "px";
} else {
arrowbox.pack = "start";
arrowbox.style.marginLeft = this.alignmentOffset + "px";
}
arrowbox.style.transform = "translate(" + -offset + "px, 0)";
if (position.indexOf("before_") == 0) {
container.dir = "reverse";
@ -568,6 +566,7 @@
this.setAttribute("side", "top");
}
}
arrow.hidden = false;
]]></body>
</method>
@ -579,23 +578,6 @@
]]></handler>
<handler event="popupshown" phase="target"><![CDATA[
this.setAttribute("panelopen", "true");
// Allow anchoring to a specified element inside the anchor.
var anchorClass = this.getAttribute("anonanchorclass");
if (anchorClass && this.anchorNode) {
let anchor =
document.getAnonymousElementByAttribute(this.anchorNode, "class",
anchorClass);
if (anchor) {
let offsetX = anchor.boxObject.width / 2;
if (this.alignmentPosition.endsWith("_end"))
offsetX *= -1;
this.popupBoxObject.moveToAnchor(anchor, this.alignmentPosition,
offsetX, 0,
false);
this.adjustArrowPosition();
}
}
]]></handler>
<handler event="popuphidden" phase="target"><![CDATA[
this.removeAttribute("panelopen");

View File

@ -86,6 +86,7 @@ GK_ATOM(alternate, "alternate")
GK_ATOM(always, "always")
GK_ATOM(ancestor, "ancestor")
GK_ATOM(ancestorOrSelf, "ancestor-or-self")
GK_ATOM(anchor, "anchor")
GK_ATOM(_and, "and")
GK_ATOM(any, "any")
GK_ATOM(mozapp, "mozapp")

View File

@ -668,6 +668,27 @@ nsMenuFrame::AttributeChanged(int32_t aNameSpaceID,
return NS_OK;
}
nsIContent*
nsMenuFrame::GetAnchor()
{
mozilla::dom::Element* anchor = nullptr;
nsAutoString id;
mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::anchor, id);
if (!id.IsEmpty()) {
nsIDocument* doc = mContent->OwnerDoc();
anchor =
doc->GetAnonymousElementByAttribute(mContent, nsGkAtoms::anonid, id);
if (!anchor) {
anchor = doc->GetElementById(id);
}
}
// Always return the menu's content if the anchor wasn't set or wasn't found.
return anchor && anchor->GetPrimaryFrame() ? anchor : mContent;
}
void
nsMenuFrame::OpenMenu(bool aSelectFirstItem)
{
@ -725,7 +746,7 @@ nsMenuFrame::DoLayout(nsBoxLayoutState& aState)
nsMenuPopupFrame* popupFrame = GetPopup();
if (popupFrame) {
bool sizeToPopup = IsSizedToPopup(mContent, false);
popupFrame->LayoutPopup(aState, this, sizeToPopup);
popupFrame->LayoutPopup(aState, this, GetAnchor()->GetPrimaryFrame(), sizeToPopup);
}
return rv;

View File

@ -128,6 +128,12 @@ public:
virtual nsIScrollableFrame* GetScrollTargetFrame() MOZ_OVERRIDE;
// Retrieve the element that the menu should be anchored to. By default this is
// the menu itself. However, the anchor attribute may refer to the value of an
// anonid within the menu's binding, or, if not found, the id of an element in
// the document.
nsIContent* GetAnchor();
/**
* NOTE: OpenMenu will open the menu asynchronously.
*/

View File

@ -379,7 +379,8 @@ nsMenuPopupFrame::IsLeaf() const
}
void
nsMenuPopupFrame::LayoutPopup(nsBoxLayoutState& aState, nsIFrame* aParentMenu, bool aSizedToPopup)
nsMenuPopupFrame::LayoutPopup(nsBoxLayoutState& aState, nsIFrame* aParentMenu,
nsIFrame* aAnchor, bool aSizedToPopup)
{
if (!mGeneratedChildren)
return;
@ -427,7 +428,7 @@ nsMenuPopupFrame::LayoutPopup(nsBoxLayoutState& aState, nsIFrame* aParentMenu, b
}
if (shouldPosition) {
SetPopupPosition(aParentMenu, false);
SetPopupPosition(aAnchor, false, aSizedToPopup);
}
nsRect bounds(GetRect());
@ -444,7 +445,7 @@ nsMenuPopupFrame::LayoutPopup(nsBoxLayoutState& aState, nsIFrame* aParentMenu, b
// so set the preferred size accordingly
mPrefSize = newsize;
if (isOpen) {
SetPopupPosition(nullptr, false);
SetPopupPosition(nullptr, false, aSizedToPopup);
}
}
}
@ -1120,7 +1121,7 @@ nsMenuPopupFrame::FlipOrResize(nscoord& aScreenPoint, nscoord aSize,
}
nsresult
nsMenuPopupFrame::SetPopupPosition(nsIFrame* aAnchorFrame, bool aIsMove)
nsMenuPopupFrame::SetPopupPosition(nsIFrame* aAnchorFrame, bool aIsMove, bool aSizedToPopup)
{
if (!mShouldAutoPosition)
return NS_OK;
@ -1152,12 +1153,6 @@ nsMenuPopupFrame::SetPopupPosition(nsIFrame* aAnchorFrame, bool aIsMove)
}
}
bool sizedToPopup = false;
if (aAnchorFrame->GetContent()) {
// the popup should be the same size as the anchor menu, for example, a menulist.
sizedToPopup = nsMenuFrame::IsSizedToPopup(aAnchorFrame->GetContent(), false);
}
// the dimensions of the anchor in its app units
nsRect parentRect = aAnchorFrame->GetScreenRectInAppUnits();
@ -1174,7 +1169,7 @@ nsMenuPopupFrame::SetPopupPosition(nsIFrame* aAnchorFrame, bool aIsMove)
// width. The preferred size should already be set by the parent frame.
NS_ASSERTION(mPrefSize.width >= 0 || mPrefSize.height >= 0,
"preferred size of popup not set");
mRect.width = sizedToPopup ? parentRect.width : mPrefSize.width;
mRect.width = aSizedToPopup ? parentRect.width : mPrefSize.width;
mRect.height = mPrefSize.height;
// the screen position in app units where the popup should appear
@ -1370,7 +1365,7 @@ nsMenuPopupFrame::SetPopupPosition(nsIFrame* aAnchorFrame, bool aIsMove)
// Now that we've positioned the view, sync up the frame's origin.
nsBoxFrame::SetPosition(viewPoint - GetParent()->GetOffsetTo(rootFrame));
if (sizedToPopup) {
if (aSizedToPopup) {
nsBoxLayoutState state(PresContext());
// XXXndeakin can parentSize.width still extend outside?
SetBounds(state, nsRect(mRect.x, mRect.y, parentRect.width, mRect.height));
@ -1934,7 +1929,7 @@ nsMenuPopupFrame::MoveTo(int32_t aLeft, int32_t aTop, bool aUpdateAttrs)
mScreenXPos = aLeft - presContext->AppUnitsToIntCSSPixels(margin.left);
mScreenYPos = aTop - presContext->AppUnitsToIntCSSPixels(margin.top);
SetPopupPosition(nullptr, true);
SetPopupPosition(nullptr, true, false);
nsCOMPtr<nsIContent> popup = mContent;
if (aUpdateAttrs && (popup->HasAttr(kNameSpaceID_None, nsGkAtoms::left) ||
@ -1962,7 +1957,7 @@ nsMenuPopupFrame::MoveToAnchor(nsIContent* aAnchorContent,
mPopupState = ePopupOpenAndVisible;
// Pass false here so that flipping and adjusting to fit on the screen happen.
SetPopupPosition(nullptr, false);
SetPopupPosition(nullptr, false, false);
}
bool

View File

@ -203,7 +203,8 @@ public:
virtual bool IsLeaf() const MOZ_OVERRIDE;
// layout, position and display the popup as needed
void LayoutPopup(nsBoxLayoutState& aState, nsIFrame* aParentMenu, bool aSizedToPopup);
void LayoutPopup(nsBoxLayoutState& aState, nsIFrame* aParentMenu,
nsIFrame* aAnchor, bool aSizedToPopup);
nsView* GetRootViewForPopup(nsIFrame* aStartFrame);
@ -212,7 +213,7 @@ public:
// point if a screen position (mScreenXPos and mScreenYPos) are set. The popup
// will be adjusted so that it is on screen. If aIsMove is true, then the popup
// is being moved, and should not be flipped.
nsresult SetPopupPosition(nsIFrame* aAnchorFrame, bool aIsMove);
nsresult SetPopupPosition(nsIFrame* aAnchorFrame, bool aIsMove, bool aSizedToPopup);
bool HasGeneratedChildren() { return mGeneratedChildren; }
void SetGeneratedChildren() { mGeneratedChildren = true; }

View File

@ -130,7 +130,7 @@ nsPopupSetFrame::DoLayout(nsBoxLayoutState& aState)
// lay out all of our currently open popups.
for (nsFrameList::Enumerator e(mPopupList); !e.AtEnd(); e.Next()) {
nsMenuPopupFrame* popupChild = static_cast<nsMenuPopupFrame*>(e.get());
popupChild->LayoutPopup(aState, nullptr, false);
popupChild->LayoutPopup(aState, nullptr, nullptr, false);
}
return rv;

View File

@ -342,7 +342,7 @@ nsXULPopupManager::AdjustPopupsOnWindowChange(nsPIDOMWindow* aWindow)
if (window) {
window = window->GetPrivateRoot();
if (window == aWindow) {
frame->SetPopupPosition(nullptr, true);
frame->SetPopupPosition(nullptr, true, false);
}
}
}
@ -395,7 +395,7 @@ nsXULPopupManager::PopupMoved(nsIFrame* aFrame, nsIntPoint aPnt)
// the specified screen coordinates.
if (menuPopupFrame->IsAnchored() &&
menuPopupFrame->PopupLevel() == ePopupLevelParent) {
menuPopupFrame->SetPopupPosition(nullptr, true);
menuPopupFrame->SetPopupPosition(nullptr, true, false);
}
else {
menuPopupFrame->MoveTo(aPnt.x, aPnt.y, false);
@ -605,7 +605,7 @@ nsXULPopupManager::ShowMenu(nsIContent *aMenu,
// there is no trigger event for menus
InitTriggerEvent(nullptr, nullptr, nullptr);
popupFrame->InitializePopup(aMenu, nullptr, position, 0, 0, true);
popupFrame->InitializePopup(menuFrame->GetAnchor(), nullptr, position, 0, 0, true);
if (aAsynchronous) {
nsCOMPtr<nsIRunnable> event =

View File

@ -97,6 +97,7 @@ skip-if = os == "win" # Intermittent failures, bug 919016
[test_keys.xul]
[test_largemenu.xul]
[test_menu.xul]
[test_menu_anchored.xul]
[test_menu_hide.xul]
[test_menuchecks.xul]
[test_menuitem_blink.xul]

View File

@ -0,0 +1,77 @@
<?xml version="1.0"?>
<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
<?xml-stylesheet href="chrome://mochikit/content/tests/SimpleTest/test.css" type="text/css"?>
<!--
Test for menus with the anchor attribute set
-->
<window title="Anchored Menus Test"
align="start"
onload="setTimeout(runTest, 0,'tb1');"
xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/SimpleTest.js"></script>
<script type="application/javascript" src="chrome://mochikit/content/tests/SimpleTest/EventUtils.js"></script>
<script type="application/javascript" src="xul_selectcontrol.js"/>
<hbox>
<toolbarbutton id="tb1" type="menu-button" label="Open" anchor="dropmarker">
<menupopup id="popup1"
onpopupshown="checkPopup(this, document.getAnonymousElementByAttribute(this.parentNode, 'anonid', 'dropmarker'))"
onpopuphidden="runTest('tb2')">
<menuitem label="Item"/>
</menupopup>
</toolbarbutton>
<toolbarbutton id="tb2" type="menu-button" label="Open" anchor="someanchor">
<menupopup id="popup2" onpopupshown="checkPopup(this, $('someanchor'))" onpopuphidden="runTest('tb3')">
<menuitem label="Item"/>
</menupopup>
</toolbarbutton>
<toolbarbutton id="tb3" type="menu-button" label="Open" anchor="noexist">
<menupopup id="popup3" onpopupshown="checkPopup(this, this.parentNode)" onpopuphidden="SimpleTest.finish()">
<menuitem label="Item"/>
</menupopup>
</toolbarbutton>
</hbox>
<hbox pack="end" width="180">
<button id="someanchor" label="Anchor"/>
</hbox>
<!-- test results are displayed in the html:body -->
<body xmlns="http://www.w3.org/1999/xhtml" style="height: 300px; overflow: auto;"/>
<script type="application/javascript"><![CDATA[
function runTest(menuid)
{
let menu = $(menuid);
let dropmarker = document.getAnonymousElementByAttribute(menu, "anonid", "dropmarker");
synthesizeMouseAtCenter(dropmarker, { });
}
function isWithinHalfPixel(a, b)
{
return Math.abs(a - b) <= 0.5;
}
function checkPopup(popup, anchor)
{
let popupRect = popup.getBoundingClientRect();
let anchorRect = anchor.getBoundingClientRect();
ok(isWithinHalfPixel(popupRect.left, anchorRect.left), popup.id + " left");
ok(isWithinHalfPixel(popupRect.top, anchorRect.bottom), popup.id + " top");
popup.hidePopup();
}
SimpleTest.waitForExplicitFinish();
]]>
</script>
</window>

View File

@ -314,7 +314,7 @@
<binding id="arrowpanel" extends="chrome://global/content/bindings/popup.xml#panel">
<content flip="both" side="top" position="bottomcenter topleft" consumeoutsideclicks="false">
<xul:box anonid="container" class="panel-arrowcontainer" flex="1"
<xul:vbox anonid="container" class="panel-arrowcontainer" flex="1"
xbl:inherits="side,panelopen">
<xul:box anonid="arrowbox" class="panel-arrowbox">
<xul:image anonid="arrow" class="panel-arrow" xbl:inherits="side"/>
@ -323,7 +323,7 @@
<children/>
<xul:box class="panel-inner-arrowcontentfooter" xbl:inherits="footertype" hidden="true"/>
</xul:box>
</xul:box>
</xul:vbox>
</content>
<implementation>
<field name="_fadeTimer">null</field>
@ -382,7 +382,7 @@
// if this panel has a "sliding" arrow, we may have previously set margins...
arrowbox.style.removeProperty("transform");
if (position.indexOf("start_") == 0 || position.indexOf("end_") == 0) {
container.orient = "";
container.orient = "horizontal";
arrowbox.orient = "vertical";
if (position.indexOf("_after") > 0) {
arrowbox.pack = "end";
@ -404,7 +404,7 @@
}
}
else if (position.indexOf("before_") == 0 || position.indexOf("after_") == 0) {
container.orient = "vertical";
container.orient = "";
arrowbox.orient = "";
if (position.indexOf("_end") > 0) {
arrowbox.pack = "end";

View File

@ -34,7 +34,8 @@
xbl:inherits="value=label,accesskey,crop,dragover-top"/>
<xul:label class="toolbarbutton-multiline-text" flex="1"
xbl:inherits="xbl:text=label,accesskey"/>
<xul:dropmarker type="menu" class="toolbarbutton-menu-dropmarker" xbl:inherits="disabled,label"/>
<xul:dropmarker anonid="dropmarker" type="menu"
class="toolbarbutton-menu-dropmarker" xbl:inherits="disabled,label"/>
</content>
</binding>
@ -50,7 +51,8 @@
<xul:label class="toolbarbutton-multiline-text" flex="1"
xbl:inherits="xbl:text=label,accesskey"/>
</xul:vbox>
<xul:dropmarker type="menu" class="toolbarbutton-menu-dropmarker" xbl:inherits="disabled,label"/>
<xul:dropmarker anonid="dropmarker" type="menu"
class="toolbarbutton-menu-dropmarker" xbl:inherits="disabled,label"/>
</xul:hbox>
</content>
</binding>
@ -68,7 +70,7 @@
xbl:inherits="disabled,crop,image,label,accesskey,command,wrap,
align,dir,pack,orient,tooltiptext=buttontooltiptext"/>
<xul:dropmarker type="menu-button" class="toolbarbutton-menubutton-dropmarker"
xbl:inherits="align,dir,pack,orient,disabled,label,open"/>
anonid="dropmarker" xbl:inherits="align,dir,pack,orient,disabled,label,open"/>
</content>
</binding>