Bug 798226 - allow the anchor arrow on a panel to move while the popup is open. r=enndeakin
☠☠ backed out by 52b2bf6fc12e ☠ ☠
authorMark Hammond <mhammond@skippinet.com.au>
Fri, 21 Dec 2012 12:26:32 +1100
changeset 116741 c928f50fe4fcf2922c613256e0afa2e04ad7315c
parent 116740 f9ea385ca51f084cab27534157ef61a9dbb21ae6
child 116742 cca7f05e2c053daeedd7a95e843fe29f740db8e4
push id24072
push userMs2ger@gmail.com
push dateSat, 22 Dec 2012 13:18:22 +0000
treeherdermozilla-central@ea373e534245 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersenndeakin
bugs798226
milestone20.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Bug 798226 - allow the anchor arrow on a panel to move while the popup is open. r=enndeakin
layout/xul/base/src/nsMenuPopupFrame.cpp
toolkit/content/tests/widgets/Makefile.in
toolkit/content/tests/widgets/test_popupanchor.xul
toolkit/content/widgets/popup.xml
--- a/layout/xul/base/src/nsMenuPopupFrame.cpp
+++ b/layout/xul/base/src/nsMenuPopupFrame.cpp
@@ -550,16 +550,18 @@ nsMenuPopupFrame::InitializePopup(nsICon
 
   mPopupState = ePopupShowing;
   mAnchorContent = aAnchorContent;
   mTriggerContent = aTriggerContent;
   mXPos = aXPos;
   mYPos = aYPos;
   mAdjustOffsetForContextMenu = false;
   mPosition = POPUPPOSITION_UNKNOWN;
+  mVFlip = false;
+  mHFlip = false;
 
   // if aAttributesOverride is true, then the popupanchor, popupalign and
   // position attributes on the <popup> override those values passed in.
   // If false, those attributes are only used if the values passed in are empty
   if (aAnchorContent) {
     nsAutoString anchor, align, position, flip;
     mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::popupanchor, anchor);
     mContent->GetAttr(kNameSpaceID_None, nsGkAtoms::popupalign, align);
@@ -1178,22 +1180,29 @@ nsMenuPopupFrame::SetPopupPosition(nsIFr
     }
     else {
       // with no anchor, the popup is positioned relative to the root frame
       anchorRect = rootScreenRect;
       screenPoint = anchorRect.TopLeft() + nsPoint(margin.left, margin.top);
     }
 
     // mXPos and mYPos specify an additonal offset passed to OpenPopup that
-    // should be added to the position
-    if (IsDirectionRTL())
-      screenPoint.x -= presContext->CSSPixelsToAppUnits(mXPos);
-    else
-      screenPoint.x += presContext->CSSPixelsToAppUnits(mXPos);
-    screenPoint.y += presContext->CSSPixelsToAppUnits(mYPos);
+    // should be added to the position.  We also add the offset to the anchor
+    // pos so a later flip/resize takes the offset into account.
+    nscoord anchorXOffset = presContext->CSSPixelsToAppUnits(mXPos);
+    if (IsDirectionRTL()) {
+      screenPoint.x -= anchorXOffset;
+      anchorRect.x -= anchorXOffset;
+    } else {
+      screenPoint.x += anchorXOffset;
+      anchorRect.x += anchorXOffset;
+    }
+    nscoord anchorYOffset = presContext->CSSPixelsToAppUnits(mYPos);
+    screenPoint.y += anchorYOffset;
+    anchorRect.y += anchorYOffset;
 
     // If this is a noautohide popup, set the screen coordinates of the popup.
     // This way, the popup stays at the location where it was opened even when
     // the window is moved. Popups at the parent level follow the parent
     // window as it is moved and remained anchored, so we want to maintain the
     // anchoring instead.
     if (IsNoAutoHide() && PopupLevel(true) != ePopupLevelParent) {
       // Account for the margin that will end up being added to the screen coordinate
--- a/toolkit/content/tests/widgets/Makefile.in
+++ b/toolkit/content/tests/widgets/Makefile.in
@@ -16,16 +16,17 @@ include $(DEPTH)/config/autoconf.mk
 		tree_shared.js \
 		$(NULL)
 
 MOCHITEST_FILES =	\
 		test_contextmenu_nested.xul \
 		test_tree_column_reorder.xul \
 		tree_shared.js \
 		test_mousecapture_area.html \
+		test_popupanchor.xul \
 		popup_shared.js \
 		test_videocontrols.html \
 		test_videocontrols_video_direction.html \
 		test_videocontrols_audio_direction.html \
 		test_audiocontrols_dimensions.html \
 		videocontrols_direction-1-ref.html \
 		videocontrols_direction-1a.html \
 		videocontrols_direction-1b.html \
new file mode 100644
--- /dev/null
+++ b/toolkit/content/tests/widgets/test_popupanchor.xul
@@ -0,0 +1,271 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="/tests/SimpleTest/test.css" type="text/css"?>
+
+<window title="Popup Anchor Tests"
+  xmlns:html="http://www.w3.org/1999/xhtml"
+  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <title>Popup Popup Tests</title>
+
+  <panel id="testPanel"
+         type="arrow"
+         noautohide="true">
+  </panel>
+
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"></script>      
+
+<script>
+<![CDATA[
+var anchor, panel, arrow;
+
+function isArrowPositionedOn(side, offset) {
+  var arrowRect = arrow.getBoundingClientRect();
+  var arrowMidX = (arrowRect.left + arrowRect.right) / 2;
+  var arrowMidY = (arrowRect.top + arrowRect.bottom) / 2;
+  var panelRect = panel.getBoundingClientRect();
+  var panelMidX = (panelRect.left + panelRect.right) / 2;
+  var panelMidY = (panelRect.top + panelRect.bottom) / 2;
+  // First check the "flip" of the panel is correct.  If we are expecting the
+  // arrow to be pointing to the left side of the anchor, the arrow must
+  // also be on the left side of the panel (and vice-versa)
+  // XXX - on OSX, the arrow seems to always be exactly in the center, hence
+  // the 'equals' sign in the "<=" and ">=" comparisons.  NFI why though...
+  switch (side) {
+    case "left":
+      ok(arrowMidX <= panelMidX, "arrow should be on the left of the panel");
+      break;
+    case "right":
+      ok(arrowMidX >= panelMidX, "arrow should be on the right of the panel");
+      break;
+    case "top":
+      ok(arrowMidY <= panelMidY, "arrow should be on the top of the panel");
+      break;
+    case "bottom":
+      ok(arrowMidY >= panelMidY, "arrow should be on the bottom of the panel");
+      break;
+    default:
+      ok(false, "invalid position " + where);
+      break;
+  }
+  function is_close(got, exp, msg) {
+    // on some platforms we see differences of a fraction of a pixel - so
+    // allow any difference of < 1 pixels as being OK.
+    ok(Math.abs(got - exp) < 1, msg + ": " + got + " should be equal to " + exp);
+  }
+  // Now check the arrow really is pointing where we expect.  The middle of
+  // the arrow should be pointing exactly to the left (or right) side of the
+  // anchor rect, +- any offsets.
+  offset = offset || 0; // no param means no offset expected.
+  var anchorRect = anchor.getBoundingClientRect();
+  var anchorPos = anchorRect[side];
+  switch (side) {
+    case "left":
+    case "right":
+      is_close(arrowMidX - anchorPos, offset, "arrow should be " + offset + "px from " + side + " side of anchor");
+      is_close(panelRect.top, anchorRect.bottom, "top of panel should be at bottom of anchor");
+      break;
+    case "top":
+    case "bottom":
+      is_close(arrowMidY - anchorPos, offset, "arrow should be " + offset + "px from " + side + " side of anchor");
+      is_close(panelRect.right, anchorRect.left, "right of panel should be left of anchor");
+      break;
+    default:
+      ok(false, "unknown side " + side);
+      break;
+  }
+}
+
+function openPopup(position, callback) {
+  panel.addEventListener("popupshown", function popupshown() {
+    panel.removeEventListener("popupshown", popupshown);
+    callback();
+  }, false);
+  panel.openPopup(anchor, position);
+}
+
+var tests = {
+  // A panel with the anchor after_end - the anchor should not move on resize
+  simpleResizeHorizontal: function(next, bHorizontal) {
+    openPopup("after_end", function() {
+      isArrowPositionedOn("right");
+      var origPanelRect = panel.getBoundingClientRect();
+      panel.sizeTo(100, 100);
+      isArrowPositionedOn("right"); // should not have flipped, so still "right"
+      panel.sizeTo(origPanelRect.width, origPanelRect.height);
+      isArrowPositionedOn("right"); // should not have flipped, so still "right"
+      next();
+    });
+  },
+
+  simpleResizeVertical: function(next) {
+    openPopup("start_after", function() {
+      isArrowPositionedOn("bottom");
+      var origPanelRect = panel.getBoundingClientRect();
+      panel.sizeTo(100, 100);
+      isArrowPositionedOn("bottom"); // should not have flipped
+      panel.sizeTo(origPanelRect.width, origPanelRect.height);
+      isArrowPositionedOn("bottom"); // should not have flipped
+      next();
+    });
+  },
+
+  flippingResizeHorizontal: function(next, bHorizontal) {
+    openPopup("after_end", function() {
+      isArrowPositionedOn("right");
+      panel.sizeTo(anchor.getBoundingClientRect().left + 50, 50);
+      isArrowPositionedOn("left"); // check it flipped and has zero offset.
+      next();
+    });
+  },
+
+  flippingResizeVertical: function(next, bHorizontal) {
+    openPopup("start_after", function() {
+      isArrowPositionedOn("bottom");
+      panel.sizeTo(50, anchor.getBoundingClientRect().top + 50);
+      isArrowPositionedOn("top"); // check it flipped and has zero offset.
+      next();
+    });
+  },
+
+  simpleMoveToAnchorHorizontal: function(next) {
+    openPopup("after_end", function() {
+      isArrowPositionedOn("right");
+      panel.moveToAnchor(anchor, "after_end", 20, 0);
+      // the anchor and the panel should have moved 20px right without flipping.
+      isArrowPositionedOn("right", 20);
+      panel.moveToAnchor(anchor, "after_end", -20, 0);
+      // the anchor and the panel should have moved 20px left without flipping.
+      isArrowPositionedOn("right", -20);
+      next();
+    });
+  },
+
+  simpleMoveToAnchorVertical: function(next) {
+    openPopup("start_after", function() {
+      isArrowPositionedOn("bottom");
+      panel.moveToAnchor(anchor, "start_after", 0, 20);
+      // the anchor and the panel should have moved 20px down without flipping.
+      isArrowPositionedOn("bottom", 20);
+      panel.moveToAnchor(anchor, "start_after", 0, -20);
+      // the anchor and the panel should have moved 20px up without flipping.
+      isArrowPositionedOn("bottom", -20);
+      next();
+    });
+  },
+
+  // Do a moveToAnchor that causes the panel to flip horizontally
+  flippingMoveToAnchorHorizontal: function(next) {
+    var anchorRight = anchor.getBoundingClientRect().right;
+    // Size the panel such that it only just fits from the left-hand side of
+    // the window to the right of the anchor - thus, it will fit when
+    // anchored to the right-hand side of the anchor.
+    panel.sizeTo(anchorRight - 10, 100);
+    openPopup("after_end", function() {
+      isArrowPositionedOn("right");
+      // Ask for it to be anchored 1/2 way between the left edge of the window
+      // and the anchor right - it can't fit with the panel on the left/arrow
+      // on the right, so it must flip (arrow on the left, panel on the right)
+      var offset = Math.floor(-anchorRight / 2);
+      panel.moveToAnchor(anchor, "after_end", offset, 0);
+      isArrowPositionedOn("left", offset); // should have flipped and have the offset.
+      // resize back to original and move to a zero offset - it should flip back.
+      panel.sizeTo(anchorRight - 10, 100);
+      panel.moveToAnchor(anchor, "after_end", 0, 0);
+      isArrowPositionedOn("right"); // should have flipped back and no offset
+      next();
+    });
+  },
+
+  // Do a moveToAnchor that causes the panel to flip vertically
+  flippingMoveToAnchorVertical: function(next) {
+    var anchorBottom = anchor.getBoundingClientRect().bottom;
+    // See comments above in flippingMoveToAnchorHorizontal, but read
+    // "top/bottom" instead of "left/right"
+    panel.sizeTo(100, anchorBottom - 10);
+    openPopup("start_after", function() {
+      isArrowPositionedOn("bottom");
+      var offset = Math.floor(-anchorBottom / 2);
+      panel.moveToAnchor(anchor, "start_after", 0, offset);
+      isArrowPositionedOn("top", offset);
+      panel.sizeTo(100, anchorBottom - 10);
+      panel.moveToAnchor(anchor, "start_after", 0, 0);
+      isArrowPositionedOn("bottom");
+      next();
+    });
+  },
+
+  veryWidePanel: function(next) {
+    openPopup("after_end", function() {
+      var origArrowRect = arrow.getBoundingClientRect();
+      // Now move it such that the arrow can't be at either end of the panel but
+      // instead somewhere in the middle as that is the only way things fit.
+      // XXX - these tests might not be quite correct even when bug 812943
+      // is fixed.
+      panel.sizeTo(window.innerWidth - 10, 60);
+      todo_is(panel.getBoundingClientRect().width, window.innerWidth - 10, "Bug 812943 - width is what we requested.")
+      // the arrow should not have moved.
+      var curArrowRect = arrow.getBoundingClientRect();
+      todo_is(curArrowRect.left, origArrowRect.left, "Bug 812943 - arrow should not have moved");
+      is(curArrowRect.top, origArrowRect.top, "arrow should not have moved up or down");
+      next();
+    });
+  }
+}
+
+function runTest() {
+  panel.sizeTo(50, 50);
+
+  var testIter = Iterator(tests);
+
+  function runNextTest() {
+    var name, func;
+    try {
+      [name, func] = testIter.next();
+    } catch (err if err instanceof StopIteration) {
+      // out of tests
+      SimpleTest.finish();
+      return;
+    }
+    SimpleTest.info("sub-test " + name + " starting");
+    try {
+      func.call(tests, function() {
+        setTimeout(function() {
+          panel.hidePopup();
+          panel.sizeTo(50, 50);
+          runNextTest();
+        }, 0);
+      });
+    } catch (ex) {
+      SimpleTest.ok(false, "sub-test " + name + " failed: " + ex.toString() +"\n"+ex.stack);
+      runNextTest();
+    }
+  }
+  runNextTest();
+}
+
+SimpleTest.waitForExplicitFinish();
+
+addEventListener("load", function() {
+  anchor = document.getElementById("anchor");
+  panel = document.getElementById("testPanel");
+  arrow = document.getAnonymousElementByAttribute(panel, "anonid", "arrow");
+  // Cancel the arrow panel slide-in transition (bug 767133) so the size and
+  // position are "stable" enough to test without jumping through hoops...
+  arrow.style.transition = "none";
+  runTest();
+});
+
+]]>
+</script>
+
+<body xmlns="http://www.w3.org/1999/xhtml">
+<!-- Our tests assume at least 100px around the anchor on all sides, else the
+     panel may flip when we don't expect it to
+-->
+<div style="margin: 100px 100px 100px 100px;">
+  <p id="display">The anchor --&gt; <span id="anchor">v</span> &lt;--</p>
+</div>
+</body>
+
+</window>
--- a/toolkit/content/widgets/popup.xml
+++ b/toolkit/content/widgets/popup.xml
@@ -335,20 +335,55 @@
         <xul:box class="panel-arrowcontent" xbl:inherits="side,align,dir,orient,pack" flex="1">
           <children/>
           <xul:box class="panel-inner-arrowcontentfooter" xbl:inherits="footertype" hidden="true"/>
         </xul:box>
       </xul:box>
     </content>
     <implementation>
       <field name="_fadeTimer">null</field>
-    </implementation>
-    <handlers>
-      <handler event="popupshowing" phase="target">
-      <![CDATA[
+      <method name="sizeTo">
+        <parameter name="aWidth"/>
+        <parameter name="aHeight"/>
+        <body>
+        <![CDATA[
+          this.popupBoxObject.sizeTo(aWidth, aHeight);
+          if (this.state == "open")
+            this.adjustArrowPosition();
+        ]]>
+        </body>
+      </method>
+      <method name="moveTo">
+        <parameter name="aLeft"/>
+        <parameter name="aTop"/>
+        <body>
+        <![CDATA[
+          this.popupBoxObject.moveTo(aLeft, aTop);
+          if (this.state == "open")
+            this.adjustArrowPosition();
+        ]]>
+        </body>
+      </method>
+      <method name="moveToAnchor">
+        <parameter name="aAnchorElement"/>
+        <parameter name="aPosition"/>
+        <parameter name="aX"/>
+        <parameter name="aY"/>
+        <parameter name="aAttributesOverride"/>
+        <body>
+        <![CDATA[
+          this.popupBoxObject.moveToAnchor(aAnchorElement, aPosition, aX, aY, aAttributesOverride);
+          if (this.state == "open")
+            this.adjustArrowPosition();
+        ]]>
+        </body>
+      </method>
+      <method name="adjustArrowPosition">
+        <body>
+        <![CDATA[
         var arrow = document.getAnonymousElementByAttribute(this, "anonid", "arrow");
 
         var anchor = this.anchorNode;
         if (!anchor) {
           arrow.hidden = true;
           return;
         }
 
@@ -384,17 +419,24 @@
           }
           else {
             container.dir = "";
             this.setAttribute("side", "top");
           }
         }
 
         arrow.hidden = false;
-
+        ]]>
+        </body>
+      </method>
+    </implementation>
+    <handlers>
+      <handler event="popupshowing" phase="target">
+      <![CDATA[
+        this.adjustArrowPosition();
         // set fading
         var fade = this.getAttribute("fade");
         var fadeDelay = (fade == "fast") ? 1 : fade == "slow" ? 4000 : 0;
         if (fadeDelay) {
           this._fadeTimer = setTimeout(function (self) {
             self.style.opacity = 0.2;
           }, fadeDelay, this);
         }
@@ -503,9 +545,8 @@
     <content>
       <xul:hbox class="popup-internal-box" flex="1" orient="vertical" style="overflow: auto;">
         <children/>
       </xul:hbox>
     </content>
   </binding>
 
 </bindings>
-