Bug 1276976, ensure that popups are visible in the right area, r=tn, r=mrbkap
☠☠ backed out by 8126ffafaf6d ☠ ☠
authorNeil Deakin <neil@mozilla.com>
Thu, 09 Jun 2016 07:59:31 -0400
changeset 301336 e7a3ba795e1a1f0547505e2c240baef183243fd3
parent 301335 435c691340a05280302417b3ba150a317cc16c08
child 301337 ed8f19f31b54a554d04ae5b23e154adad5fc3e64
push id30333
push usercbook@mozilla.com
push dateFri, 10 Jun 2016 13:39:58 +0000
treeherdermozilla-central@52679ce4756c [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerstn, mrbkap
bugs1276976
milestone50.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 1276976, ensure that popups are visible in the right area, r=tn, r=mrbkap
browser/base/content/test/general/browser_selectpopup.js
dom/webidl/PopupBoxObject.webidl
layout/base/nsIPresShell.h
layout/base/nsPresShell.cpp
layout/xul/PopupBoxObject.cpp
layout/xul/PopupBoxObject.h
layout/xul/nsMenuPopupFrame.cpp
layout/xul/nsMenuPopupFrame.h
toolkit/content/widgets/popup.xml
toolkit/modules/SelectParentHelper.jsm
xpfe/appshell/nsWebShellWindow.cpp
--- a/browser/base/content/test/general/browser_selectpopup.js
+++ b/browser/base/content/test/general/browser_selectpopup.js
@@ -350,8 +350,58 @@ add_task(function* test_event_order() {
     });
 
     EventUtils.synthesizeKey("KEY_ArrowDown", { code: "ArrowDown" });
     yield hideSelectPopup(selectPopup, false);
     yield eventsPromise;
   });
 });
 
+// This test checks select elements with a large number of options to ensure that
+// the popup appears within the browser area.
+add_task(function* test_large_popup() {
+  const pageUrl = "data:text/html," + escape(PAGECONTENT_SMALL);
+  let tab = yield BrowserTestUtils.openNewForegroundTab(gBrowser, pageUrl);
+
+  yield ContentTask.spawn(tab.linkedBrowser, null, function*() {
+    let doc = content.document;
+    let select = doc.getElementById("one");
+    for (var i = 0; i < 180; i++) {
+      select.add(new content.Option("Test" + i));
+    }
+
+    select.focus();
+  });
+
+  let selectPopup = document.getElementById("ContentSelectDropdown").menupopup;
+  let browserRect = tab.linkedBrowser.getBoundingClientRect();
+
+  let positions = [
+    "margin-top: 300px;",
+    "position: fixed; bottom: 100px;",
+    "width: 100%; height: 9999px;"
+  ];
+
+  let position;
+  while (true) {
+    yield openSelectPopup(selectPopup, false);
+
+    let rect = selectPopup.getBoundingClientRect();
+    ok(rect.top >= browserRect.top, "Popup top position in within browser area");
+    ok(rect.bottom <= browserRect.bottom, "Popup bottom position in within browser area");
+
+    yield hideSelectPopup(selectPopup, false);
+
+    position = positions.shift();
+    if (!position) {
+      break;
+    }
+
+    let contentPainted = BrowserTestUtils.contentPainted(tab.linkedBrowser);
+    yield ContentTask.spawn(tab.linkedBrowser, position, function*(position) {
+      let select = content.document.getElementById("one");
+      select.setAttribute("style", position);
+    });
+    yield contentPainted;
+  }
+
+  yield BrowserTestUtils.removeTab(tab);
+});
--- a/dom/webidl/PopupBoxObject.webidl
+++ b/dom/webidl/PopupBoxObject.webidl
@@ -182,9 +182,10 @@ interface PopupBoxObject : BoxObject
                     boolean attributesOverride);
 
   /** Returns the alignment position where the popup has appeared relative to its
    *  anchor node or point, accounting for any flipping that occurred.
    */
   readonly attribute DOMString alignmentPosition;
   readonly attribute long alignmentOffset;
 
+  void setConstraintRect(DOMRectReadOnly rect);
 };
--- a/layout/base/nsIPresShell.h
+++ b/layout/base/nsIPresShell.h
@@ -127,23 +127,26 @@ class SourceSurface;
 // when assigning capture, ignore whether capture is allowed or not
 #define CAPTURE_IGNOREALLOWED 1
 // true if events should be targeted at the capturing content or its children
 #define CAPTURE_RETARGETTOELEMENT 2
 // true if the current capture wants drags to be prevented
 #define CAPTURE_PREVENTDRAG 4
 // true when the mouse is pointer locked, and events are sent to locked element
 #define CAPTURE_POINTERLOCK 8
+// true when events shouldn't be retargeted to a descendant popup when capturing
+#define CAPTURE_PREVENTPOPUPRETARGET 16
 
 typedef struct CapturingContentInfo {
   // capture should only be allowed during a mousedown event
   bool mAllowed;
   bool mPointerLock;
   bool mRetargetToElement;
   bool mPreventDrag;
+  bool mPreventPopupRetarget;
   mozilla::StaticRefPtr<nsIContent> mContent;
 } CapturingContentInfo;
 
 // a75573d6-34c8-4485-8fb7-edcb6fc70e12
 #define NS_IPRESSHELL_IID \
 { 0xa75573d6, 0x34c8, 0x4485, \
   { 0x8f, 0xb7, 0xed, 0xcb, 0x6f, 0xc7, 0x0e, 0x12 } }
 
@@ -1322,16 +1325,20 @@ public:
    *
    * If CAPTURE_PREVENTDRAG is set then drags are prevented from starting while
    * this capture is active.
    *
    * If CAPTURE_POINTERLOCK is set, similar to CAPTURE_RETARGETTOELEMENT, then
    * events are targeted at aContent, but capturing is held more strongly (i.e.,
    * calls to SetCapturingContent won't unlock unless CAPTURE_POINTERLOCK is
    * set again).
+   *
+   * If CAPTURE_PREVENTPOPUPRETARGET is set, mouse events will not be retargeted
+   * to an overlapping popup. If this is not set, this retargeting can happen,
+   * as long as the popup is a descendant of aContent.
    */
   static void SetCapturingContent(nsIContent* aContent, uint8_t aFlags);
 
   /**
    * Return the active content currently capturing the mouse if any.
    */
   static nsIContent* GetCapturingContent()
   {
@@ -1342,16 +1349,25 @@ public:
    * Allow or disallow mouse capturing.
    */
   static void AllowMouseCapture(bool aAllowed)
   {
     gCaptureInfo.mAllowed = aAllowed;
   }
 
   /**
+   * Allow updating of some of the capturing information.
+   */
+  static void UpdateCapturingInfo(uint8_t aFlags, bool aNewValue) {
+    if (aFlags & CAPTURE_PREVENTPOPUPRETARGET) {
+      gCaptureInfo.mPreventPopupRetarget = aNewValue;
+    }
+  }
+
+  /**
    * Returns true if there is an active mouse capture that wants to prevent
    * drags.
    */
   static bool IsMouseCapturePreventingDrag()
   {
     return gCaptureInfo.mPreventDrag && gCaptureInfo.mContent;
   }
 
--- a/layout/base/nsPresShell.cpp
+++ b/layout/base/nsPresShell.cpp
@@ -223,17 +223,17 @@ using namespace mozilla::dom;
 using namespace mozilla::gfx;
 using namespace mozilla::layers;
 using namespace mozilla::gfx;
 using namespace mozilla::layout;
 using PaintFrameFlags = nsLayoutUtils::PaintFrameFlags;
 
 CapturingContentInfo nsIPresShell::gCaptureInfo =
   { false /* mAllowed */, false /* mPointerLock */, false /* mRetargetToElement */,
-    false /* mPreventDrag */ };
+    false /* mPreventDrag */, false /* mPreventPopupRetarget */ };
 nsIContent* nsIPresShell::gKeyDownTarget;
 nsClassHashtable<nsUint32HashKey, nsIPresShell::PointerCaptureInfo>* nsIPresShell::gPointerCaptureList;
 nsClassHashtable<nsUint32HashKey, nsIPresShell::PointerInfo>* nsIPresShell::gActivePointersIds;
 
 // RangePaintInfo is used to paint ranges to offscreen buffers
 struct RangePaintInfo {
   RefPtr<nsRange> mRange;
   nsDisplayListBuilder mBuilder;
@@ -6646,16 +6646,17 @@ nsIPresShell::SetCapturingContent(nsICon
     if (aContent) {
       gCaptureInfo.mContent = aContent;
     }
     // CAPTURE_POINTERLOCK is the same as CAPTURE_RETARGETTOELEMENT & CAPTURE_IGNOREALLOWED
     gCaptureInfo.mRetargetToElement = ((aFlags & CAPTURE_RETARGETTOELEMENT) != 0) ||
                                       ((aFlags & CAPTURE_POINTERLOCK) != 0);
     gCaptureInfo.mPreventDrag = (aFlags & CAPTURE_PREVENTDRAG) != 0;
     gCaptureInfo.mPointerLock = (aFlags & CAPTURE_POINTERLOCK) != 0;
+    gCaptureInfo.mPreventPopupRetarget = (aFlags & CAPTURE_PREVENTPOPUPRETARGET) != 0;
   }
 }
 
 class AsyncCheckPointerCaptureStateCaller : public Runnable
 {
 public:
   explicit AsyncCheckPointerCaptureStateCaller(int32_t aPointerId)
     : mPointerId(aPointerId) {}
@@ -7733,16 +7734,17 @@ PresShell::HandleEvent(nsIFrame* aFrame,
       // detect a chrome generated popup.
       if (popupFrame && capturingContent &&
           EventStateManager::IsRemoteTarget(capturingContent)) {
         capturingContent = nullptr;
       }
       // If the popupFrame is an ancestor of the 'frame', the frame should
       // handle the event, otherwise, the popup should handle it.
       if (popupFrame &&
+          !gCaptureInfo.mPreventPopupRetarget &&
           !nsContentUtils::ContentIsCrossDocDescendantOf(
              framePresContext->GetPresShell()->GetDocument(),
              popupFrame->GetContent())) {
         frame = popupFrame;
       }
     }
 
     bool captureRetarget = false;
--- a/layout/xul/PopupBoxObject.cpp
+++ b/layout/xul/PopupBoxObject.cpp
@@ -352,16 +352,26 @@ PopupBoxObject::AlignmentOffset()
   // Note that the offset might be along either the X or Y axis, but for the
   // sake of simplicity we use a point with only the X axis set so we can
   // use ToNearestPixels().
   nsPoint appOffset(menuPopupFrame->GetAlignmentOffset(), 0);
   nsIntPoint popupOffset = appOffset.ToNearestPixels(pp);
   return popupOffset.x;
 }
 
+void
+PopupBoxObject::SetConstraintRect(dom::DOMRectReadOnly& aRect)
+{
+  nsMenuPopupFrame *menuPopupFrame = do_QueryFrame(GetFrame(false));
+  if (menuPopupFrame) {
+    menuPopupFrame->SetOverrideConstraintRect(
+      LayoutDeviceIntRect(aRect.Left(), aRect.Top(), aRect.Width(), aRect.Height()));
+  }
+}
+
 } // namespace dom
 } // namespace mozilla
 
 // Creation Routine ///////////////////////////////////////////////////////////////////////
 
 nsresult
 NS_NewPopupBoxObject(nsIBoxObject** aResult)
 {
--- a/layout/xul/PopupBoxObject.h
+++ b/layout/xul/PopupBoxObject.h
@@ -95,16 +95,18 @@ public:
                     int32_t aXPos,
                     int32_t aYPos,
                     bool aAttributesOverride);
 
   void GetAlignmentPosition(nsString& positionStr);
 
   int32_t AlignmentOffset();
 
+  void SetConstraintRect(dom::DOMRectReadOnly& aRect);
+
 private:
   ~PopupBoxObject();
 
 protected:
   nsPopupSetFrame* GetPopupSetFrame();
 };
 
 } // namespace dom
--- a/layout/xul/nsMenuPopupFrame.cpp
+++ b/layout/xul/nsMenuPopupFrame.cpp
@@ -1578,16 +1578,27 @@ nsMenuPopupFrame::GetConstraintRect(cons
           &screenRectPixels.width, &screenRectPixels.height);
     }
   }
 
   if (mInContentShell) {
     // for content shells, clip to the client area rather than the screen area
     screenRectPixels.IntersectRect(screenRectPixels, aRootScreenRect);
   }
+  else if (!mOverrideConstraintRect.IsEmpty()) {
+    LayoutDeviceIntRect overrideConstrainRect =
+      LayoutDeviceIntRect::FromAppUnitsToNearest(mOverrideConstraintRect,
+                                                 PresContext()->AppUnitsPerDevPixel());
+    // This is currently only used for <select> elements where we want to constrain
+    // vertically to the screen but not horizontally, so do the intersection and then
+    // reset the horizontal values.
+    screenRectPixels.IntersectRect(screenRectPixels, overrideConstrainRect);
+    screenRectPixels.x = overrideConstrainRect.x;
+    screenRectPixels.width = overrideConstrainRect.width;
+  }
 
   return screenRectPixels;
 }
 
 void nsMenuPopupFrame::CanAdjustEdges(int8_t aHorizontalSide,
                                       int8_t aVerticalSide,
                                       LayoutDeviceIntPoint& aChange)
 {
--- a/layout/xul/nsMenuPopupFrame.h
+++ b/layout/xul/nsMenuPopupFrame.h
@@ -350,16 +350,20 @@ public:
                     bool aAttributesOverride);
 
   bool GetAutoPosition();
   void SetAutoPosition(bool aShouldAutoPosition);
   void SetConsumeRollupEvent(uint32_t aConsumeMode);
 
   nsIScrollableFrame* GetScrollFrame(nsIFrame* aStart);
 
+  void SetOverrideConstraintRect(mozilla::LayoutDeviceIntRect aRect) {
+    mOverrideConstraintRect = ToAppUnits(aRect, PresContext()->AppUnitsPerCSSPixel());
+  }
+
   // For a popup that should appear anchored at the given rect, determine
   // the screen area that it is constrained by. This will be the available
   // area of the screen the popup should be displayed on. Content popups,
   // however, will also be constrained by the content area, given by
   // aRootScreenRect. All coordinates are in app units.
   // For non-toplevel popups (which will always be panels), we will also
   // constrain them to the available screen rect, ie they will not fall
   // underneath the taskbar, dock or other fixed OS elements.
@@ -583,15 +587,17 @@ protected:
 
   // the flip modes that were used when the popup was opened
   bool mHFlip;
   bool mVFlip;
 
   // How the popup is anchored.
   MenuPopupAnchorType mAnchorType;
 
+  nsRect mOverrideConstraintRect;
+
   static int8_t sDefaultLevelIsTop;
 
   // If 0, never timed out.  Otherwise, the value is in milliseconds.
   static uint32_t sTimeoutOfIncrementalSearch;
 }; // class nsMenuPopupFrame
 
 #endif
--- a/toolkit/content/widgets/popup.xml
+++ b/toolkit/content/widgets/popup.xml
@@ -218,16 +218,25 @@
 
       <method name="getOuterScreenRect">
         <body>
         <![CDATA[
           return this.popupBoxObject.getOuterScreenRect();
         ]]>
         </body>
       </method>
+
+      <method name="setConstraintRect">
+        <parameter name="aRect"/>
+        <body>
+        <![CDATA[
+          this.popupBoxObject.setConstraintRect(aRect);
+        ]]>
+        </body>
+      </method>
     </implementation>
 
   </binding>
 
   <binding id="popup" role="xul:menupopup"
            extends="chrome://global/content/bindings/popup.xml#popup-base">
 
     <content>
--- a/toolkit/modules/SelectParentHelper.jsm
+++ b/toolkit/modules/SelectParentHelper.jsm
@@ -21,16 +21,22 @@ this.SelectParentHelper = {
     populateChildren(menulist, items, selectedIndex, zoom);
   },
 
   open: function(browser, menulist, rect) {
     menulist.hidden = false;
     currentBrowser = browser;
     this._registerListeners(browser, menulist.menupopup);
 
+    let win = browser.ownerDocument.defaultView;
+    let constraintRect = browser.getBoundingClientRect();
+    constraintRect = new win.DOMRect(constraintRect.left + win.mozInnerScreenX,
+                                     constraintRect.top + win.mozInnerScreenY,
+                                     constraintRect.width, constraintRect.height);
+    menulist.menupopup.setConstraintRect(constraintRect);
     menulist.menupopup.openPopupAtScreenRect("after_start", rect.left, rect.top, rect.width, rect.height, false, false);
     menulist.selectedItem.scrollIntoView();
   },
 
   hide: function(menulist, browser) {
     if (currentBrowser == browser) {
       menulist.menupopup.hidePopup();
     }
--- a/xpfe/appshell/nsWebShellWindow.cpp
+++ b/xpfe/appshell/nsWebShellWindow.cpp
@@ -256,16 +256,21 @@ nsWebShellWindow::WindowMoved(nsIWidget*
 {
   nsXULPopupManager* pm = nsXULPopupManager::GetInstance();
   if (pm) {
     nsCOMPtr<nsPIDOMWindowOuter> window =
       mDocShell ? mDocShell->GetWindow() : nullptr;
     pm->AdjustPopupsOnWindowChange(window);
   }
 
+  nsCOMPtr<nsIPresShell> presShell = mDocShell->GetPresShell();
+  if (presShell) {
+    presShell->UpdateCapturingInfo(CAPTURE_PREVENTPOPUPRETARGET, true);
+  }
+
   // Notify all tabs that the widget moved.
   if (mDocShell && mDocShell->GetWindow()) {
     nsCOMPtr<EventTarget> eventTarget = mDocShell->GetWindow()->GetTopWindowRoot();
     nsContentUtils::DispatchChromeEvent(mDocShell->GetDocument(),
                                         eventTarget,
                                         NS_LITERAL_STRING("MozUpdateWindowPos"),
                                         false, false, nullptr);
   }
@@ -278,16 +283,22 @@ nsWebShellWindow::WindowMoved(nsIWidget*
 
 bool
 nsWebShellWindow::WindowResized(nsIWidget* aWidget, int32_t aWidth, int32_t aHeight)
 {
   nsCOMPtr<nsIBaseWindow> shellAsWin(do_QueryInterface(mDocShell));
   if (shellAsWin) {
     shellAsWin->SetPositionAndSize(0, 0, aWidth, aHeight, 0);
   }
+
+  nsCOMPtr<nsIPresShell> presShell = mDocShell->GetPresShell();
+  if (presShell) {
+    presShell->UpdateCapturingInfo(CAPTURE_PREVENTPOPUPRETARGET, true);
+  }
+
   // Persist size, but not immediately, in case this OS is firing
   // repeated size events as the user drags the sizing handle
   if (!IsLocked())
     SetPersistenceTimer(PAD_POSITION | PAD_SIZE | PAD_MISC);
   return true;
 }
 
 bool