Bug 1075089 - Move popup menu frame offset to LookAndFeel and fix default offset for OS X. r=Enn
authorNick Robson <nicko.robson@gmail.com>
Tue, 04 Aug 2015 16:41:00 -0400
changeset 289876 a68c49ad5acc93e1aa086729ed677baefb09a65a
parent 289875 5a68d5a3fdb6c545e7d6a6962766109933234a8c
child 289877 65972984187db749477a8b5fbd7a51aba769e018
push id5245
push userraliiev@mozilla.com
push dateThu, 29 Oct 2015 11:30:51 +0000
treeherdermozilla-beta@dac831dc1bd0 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewersEnn
bugs1075089
milestone43.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 1075089 - Move popup menu frame offset to LookAndFeel and fix default offset for OS X. r=Enn
layout/xul/nsMenuPopupFrame.cpp
layout/xul/nsMenuPopupFrame.h
testing/marionette/client/marionette/tests/unit/test_single_finger_desktop.py
toolkit/content/tests/chrome/popup_trigger.js
toolkit/content/tests/chrome/test_bug624329.xul
toolkit/content/tests/chrome/test_contextmenu_list.xul
toolkit/content/tests/chrome/window_largemenu.xul
widget/LookAndFeel.h
widget/android/nsLookAndFeel.cpp
widget/cocoa/nsLookAndFeel.mm
widget/gonk/nsLookAndFeel.cpp
widget/gtk/nsLookAndFeel.cpp
widget/nsXPLookAndFeel.cpp
widget/qt/nsLookAndFeel.cpp
widget/uikit/nsLookAndFeel.mm
widget/windows/nsLookAndFeel.cpp
--- a/layout/xul/nsMenuPopupFrame.cpp
+++ b/layout/xul/nsMenuPopupFrame.cpp
@@ -55,16 +55,21 @@
 #include "mozilla/dom/Element.h"
 #include "mozilla/dom/PopupBoxObject.h"
 #include <algorithm>
 
 using namespace mozilla;
 using mozilla::dom::PopupBoxObject;
 
 int8_t nsMenuPopupFrame::sDefaultLevelIsTop = -1;
+
+// XXX, kyle.yuan@sun.com, there are 4 definitions for the same purpose:
+//  nsMenuPopupFrame.h, nsListControlFrame.cpp, listbox.xml, tree.xml
+//  need to find a good place to put them together.
+//  if someone changes one, please also change the other.
 uint32_t nsMenuPopupFrame::sTimeoutOfIncrementalSearch = 1000;
 
 const char* kPrefIncrementalSearchTimeout =
   "ui.menu.incremental_search.timeout";
 
 // NS_NewMenuPopupFrame
 //
 // Wrapper for creating a new menu popup container
@@ -1165,17 +1170,17 @@ nsMenuPopupFrame::FlipOrResize(nscoord& 
           aScreenPoint = endpos + aMarginBegin;
           popupSize = aScreenEnd - aScreenPoint;
         }
       }
       else {
         // if the newly calculated position is different than the existing
         // position, we flip such that the popup is to the left or top of the
         // anchor point instead.
-        nscoord newScreenPoint = startpos - aSize - aMarginBegin - aOffsetForContextMenu;
+        nscoord newScreenPoint = startpos - aSize - aMarginBegin - std::max(aOffsetForContextMenu, 0);
         if (newScreenPoint != aScreenPoint) {
           *aFlipSide = true;
           aScreenPoint = newScreenPoint;
 
           // check if the new position is still off the left or top edge of the
           // screen. If so, resize the popup.
           if (aScreenPoint < aScreenBegin) {
             aScreenPoint = aScreenBegin;
@@ -1311,17 +1316,17 @@ nsMenuPopupFrame::SetPopupPosition(nsIFr
 
   nsMargin margin(0, 0, 0, 0);
   StyleMargin()->GetMargin(margin);
 
   // the screen rectangle of the root frame, in dev pixels.
   nsRect rootScreenRect = rootFrame->GetScreenRectInAppUnits();
 
   nsDeviceContext* devContext = presContext->DeviceContext();
-  nscoord offsetForContextMenu = 0;
+  nsPoint offsetForContextMenu;
 
   bool isNoAutoHide = IsNoAutoHide();
   nsPopupLevel popupLevel = PopupLevel(isNoAutoHide);
 
   if (anchored) {
     // if we are anchored, there are certain things we don't want to do when
     // repositioning the popup to fit on the screen, such as end up positioned
     // over the anchor, for instance a popup appearing over the menu label.
@@ -1372,35 +1377,39 @@ nsMenuPopupFrame::SetPopupPosition(nsIFr
   }
   else {
     // The popup is positioned at a screen coordinate.
     // First convert the screen position in mScreenRect from CSS pixels into
     // device pixels, ignoring any zoom as mScreenRect holds unzoomed screen
     // coordinates.
     int32_t factor = devContext->AppUnitsPerDevPixelAtUnitFullZoom();
 
-    // context menus should be offset by two pixels so that they don't appear
-    // directly where the cursor is. Otherwise, it is too easy to have the
-    // context menu close up again.
+    // Depending on the platform, context menus should be offset by varying amounts
+    // to ensure that they don't appear directly where the cursor is. Otherwise,
+    // it is too easy to have the context menu close up again.
     if (mAdjustOffsetForContextMenu) {
-      int32_t offsetForContextMenuDev =
-        nsPresContext::CSSPixelsToAppUnits(CONTEXT_MENU_OFFSET_PIXELS) / factor;
-      offsetForContextMenu = presContext->DevPixelsToAppUnits(offsetForContextMenuDev);
+      nsPoint offsetForContextMenuDev;
+      offsetForContextMenuDev.x = nsPresContext::CSSPixelsToAppUnits(LookAndFeel::GetInt(
+                                    LookAndFeel::eIntID_ContextMenuOffsetHorizontal)) / factor;
+      offsetForContextMenuDev.y = nsPresContext::CSSPixelsToAppUnits(LookAndFeel::GetInt(
+                                    LookAndFeel::eIntID_ContextMenuOffsetVertical)) / factor;
+      offsetForContextMenu.x = presContext->DevPixelsToAppUnits(offsetForContextMenuDev.x);
+      offsetForContextMenu.y = presContext->DevPixelsToAppUnits(offsetForContextMenuDev.y);
     }
 
     // next, convert into app units accounting for the zoom
     screenPoint.x = presContext->DevPixelsToAppUnits(
                       nsPresContext::CSSPixelsToAppUnits(mScreenRect.x) / factor);
     screenPoint.y = presContext->DevPixelsToAppUnits(
                       nsPresContext::CSSPixelsToAppUnits(mScreenRect.y) / factor);
     anchorRect = nsRect(screenPoint, nsSize(0, 0));
 
     // add the margins on the popup
-    screenPoint.MoveBy(margin.left + offsetForContextMenu,
-                       margin.top + offsetForContextMenu);
+    screenPoint.MoveBy(margin.left + offsetForContextMenu.x,
+                       margin.top + offsetForContextMenu.y);
 
     // screen positioned popups can be flipped vertically but never horizontally
     vFlip = FlipStyle_Outside;
   }
 
   // If a panel is being moved or has flip="none", don't constrain or flip it. But always do this for
   // content shells, so that the popup doesn't extend outside the containing frame.
   if (mInContentShell || (mFlip != FlipType_None && (!aIsMove || mPopupType != ePopupTypePanel))) {
@@ -1434,26 +1443,26 @@ nsMenuPopupFrame::SetPopupPosition(nsIFr
     // positioned at screenPoint. If not, flip the popups to the opposite side
     // of their anchor point, or resize them as necessary.
     if (slideHorizontal) {
       mRect.width = SlideOrResize(screenPoint.x, mRect.width, screenRect.x,
                                   screenRect.XMost(), &mAlignmentOffset);
     } else {
       mRect.width = FlipOrResize(screenPoint.x, mRect.width, screenRect.x,
                                  screenRect.XMost(), anchorRect.x, anchorRect.XMost(),
-                                 margin.left, margin.right, offsetForContextMenu, hFlip,
+                                 margin.left, margin.right, offsetForContextMenu.x, hFlip,
                                  &mHFlip);
     }
     if (slideVertical) {
       mRect.height = SlideOrResize(screenPoint.y, mRect.height, screenRect.y,
                                   screenRect.YMost(), &mAlignmentOffset);
     } else {
       mRect.height = FlipOrResize(screenPoint.y, mRect.height, screenRect.y,
                                   screenRect.YMost(), anchorRect.y, anchorRect.YMost(),
-                                  margin.top, margin.bottom, offsetForContextMenu, vFlip,
+                                  margin.top, margin.bottom, offsetForContextMenu.y, vFlip,
                                   &mVFlip);
     }
 
     NS_ASSERTION(screenPoint.x >= screenRect.x && screenPoint.y >= screenRect.y &&
                  screenPoint.x + mRect.width <= screenRect.XMost() &&
                  screenPoint.y + mRect.height <= screenRect.YMost(),
                  "Popup is offscreen");
   }
@@ -2153,20 +2162,20 @@ nsMenuPopupFrame::MoveTo(int32_t aLeft, 
   // and position, because the popup can be reset to its anchor position by
   // using (-1, -1) as coordinates. Subtract off the margin as it will be
   // added to the position when SetPopupPosition is called.
   nsMargin margin(0, 0, 0, 0);
   StyleMargin()->GetMargin(margin);
 
   // Workaround for bug 788189.  See also bug 708278 comment #25 and following.
   if (mAdjustOffsetForContextMenu) {
-    nscoord offsetForContextMenu =
-      nsPresContext::CSSPixelsToAppUnits(CONTEXT_MENU_OFFSET_PIXELS);
-    margin.left += offsetForContextMenu;
-    margin.top += offsetForContextMenu;
+    margin.left += nsPresContext::CSSPixelsToAppUnits(LookAndFeel::GetInt(
+                     LookAndFeel::eIntID_ContextMenuOffsetHorizontal));
+    margin.top += nsPresContext::CSSPixelsToAppUnits(LookAndFeel::GetInt(
+                     LookAndFeel::eIntID_ContextMenuOffsetVertical));
   }
 
   nsPresContext* presContext = PresContext();
   mAnchorType = aLeft == -1 || aTop == -1 ?
                 MenuPopupAnchorType_Node : MenuPopupAnchorType_Point;
   mScreenRect.x = aLeft - presContext->AppUnitsToIntCSSPixels(margin.left);
   mScreenRect.y = aTop - presContext->AppUnitsToIntCSSPixels(margin.top);
 
--- a/layout/xul/nsMenuPopupFrame.h
+++ b/layout/xul/nsMenuPopupFrame.h
@@ -117,23 +117,16 @@ enum MenuPopupAnchorType {
 #define POPUPPOSITION_STARTAFTER 6
 #define POPUPPOSITION_ENDAFTER 7
 #define POPUPPOSITION_OVERLAP 8
 #define POPUPPOSITION_AFTERPOINTER 9
 
 #define POPUPPOSITION_HFLIP(v) (v ^ 1)
 #define POPUPPOSITION_VFLIP(v) (v ^ 2)
 
-// XXX, kyle.yuan@sun.com, there are 4 definitions for the same purpose:
-//  nsMenuPopupFrame.h, nsListControlFrame.cpp, listbox.xml, tree.xml
-//  need to find a good place to put them together.
-//  if someone changes one, please also change the other.
-
-#define CONTEXT_MENU_OFFSET_PIXELS 2
-
 nsIFrame* NS_NewMenuPopupFrame(nsIPresShell* aPresShell, nsStyleContext* aContext);
 
 class nsView;
 class nsMenuPopupFrame;
 
 // this class is used for dispatching popupshown events asynchronously.
 class nsXULPopupShownEvent : public nsRunnable, public nsIDOMEventListener
 {
--- a/testing/marionette/client/marionette/tests/unit/test_single_finger_desktop.py
+++ b/testing/marionette/client/marionette/tests/unit/test_single_finger_desktop.py
@@ -70,25 +70,28 @@ prefs.setIntPref("ui.click_hold_context_
         move_element_offset(self.marionette, self.wait_for_condition, "button1-mousemove-mousedown", "button2-mousemove-mouseup")
 
     def test_wait(self):
         wait(self.marionette, self.wait_for_condition, "button1-mousemove-mousedown-mouseup-click")
 
     def test_wait_with_value(self):
         wait_with_value(self.marionette, self.wait_for_condition, "button1-mousemove-mousedown-mouseup-click")
 
+    """
+    // Skipping due to Bug 1191066
     def test_context_menu(self):
         context_menu(self.marionette, self.wait_for_condition, "button1-mousemove-mousedown-contextmenu", "button1-mousemove-mousedown-contextmenu-mouseup-click")
 
     def test_long_press_action(self):
         long_press_action(self.marionette, self.wait_for_condition, "button1-mousemove-mousedown-contextmenu-mouseup-click")
 
     def test_long_press_on_xy_action(self):
         long_press_on_xy_action(self.marionette, self.wait_for_condition, "button1-mousemove-mousedown-contextmenu-mouseup-click")
-
+    """
+    
     """
     //Skipping due to Bug 865334
     def test_long_press_fail(self):
         testAction = self.marionette.absolute_url("testAction.html")
         self.marionette.navigate(testAction)
         button = self.marionette.find_element("id", "button1Copy")
         action = Actions(self.marionette)
         action.press(button).long_press(button, 5)
--- a/toolkit/content/tests/chrome/popup_trigger.js
+++ b/toolkit/content/tests/chrome/popup_trigger.js
@@ -502,19 +502,21 @@ var popupTests = [
         is(child.documentElement.getAttribute("data"), "xnull",
            "cannot get popupNode from other document");
         child.documentElement.setAttribute("data", "none");
         // now try again with document.popupNode set explicitly
         document.popupNode = gCachedEvent.target;
       }
     }
 
+    var openX = 8;
+    var openY = 16;
     var rect = gMenuPopup.getBoundingClientRect();
-    is(rect.left, 10, testname + " left");
-    is(rect.top, 18, testname + " top");
+    is(rect.left, openX + (platformIsMac() ? 1 : 2), testname + " left");
+    is(rect.top, openY + (platformIsMac() ? -6 : 2), testname + " top");
     ok(rect.right, testname + " right is " + rect.right);
     ok(rect.bottom, testname + " bottom is " + rect.bottom);
   }
 },
 {
   // pressing a letter that doesn't correspond to an accelerator, but does
   // correspond to the first letter in a menu's label. The menu should not
   // close because there is more than one item corresponding to that letter
@@ -744,30 +746,30 @@ var popupTests = [
 },
 {
   testname: "focus and cursor down on trigger",
   condition: function() { return gIsMenu; },
   events: [ "popupshowing thepopup", "popupshown thepopup" ],
   autohide: "thepopup",
   test: function(testname, step) {
     gTrigger.focus();
-    synthesizeKey("VK_DOWN", { altKey: (navigator.platform.indexOf("Mac") == -1) });
+    synthesizeKey("VK_DOWN", { altKey: !platformIsMac() });
   },
   result: function(testname, step) {
     checkOpen("trigger", testname);
     checkActive(gMenuPopup, "", testname);
   }
 },
 {
   testname: "focus and cursor up on trigger",
   condition: function() { return gIsMenu; },
   events: [ "popupshowing thepopup", "popupshown thepopup" ],
   test: function(testname, step) {
     gTrigger.focus();
-    synthesizeKey("VK_UP", { altKey: (navigator.platform.indexOf("Mac") == -1) });
+    synthesizeKey("VK_UP", { altKey: !platformIsMac() });
   },
   result: function(testname, step) {
     checkOpen("trigger", testname);
     checkActive(gMenuPopup, "", testname);
   }
 },
 {
   testname: "select and enter on menuitem",
@@ -784,33 +786,33 @@ var popupTests = [
 },
 {
   testname: "focus trigger and key to open",
   condition: function() { return gIsMenu; },
   events: [ "popupshowing thepopup", "popupshown thepopup" ],
   autohide: "thepopup",
   test: function(testname, step) {
     gTrigger.focus();
-    synthesizeKey((navigator.platform.indexOf("Mac") == -1) ? "VK_F4" : " ", { });
+    synthesizeKey(platformIsMac() ? " " : "VK_F4", { });
   },
   result: function(testname, step) {
     checkOpen("trigger", testname);
     checkActive(gMenuPopup, "", testname);
   }
 },
 {
   // the menu should only open when the meta or alt key is not pressed
   testname: "focus trigger and key wrong modifier",
   condition: function() { return gIsMenu; },
   test: function(testname, step) {
     gTrigger.focus();
-    if (navigator.platform.indexOf("Mac") == -1)
+    if (platformIsMac())
+      synthesizeKey("VK_F4", { altKey: true });
+    else
       synthesizeKey("", { metaKey: true });
-    else
-      synthesizeKey("VK_F4", { altKey: true });
   },
   result: function(testname, step) {
     checkClosed("trigger", testname);
   }
 },
 {
   testname: "mouse click on disabled menu",
   condition: function() { return gIsMenu; },
@@ -845,8 +847,13 @@ var popupTests = [
     var submenupopup = document.getElementById("submenupopup");
     submenupopup.parentNode.removeChild(submenupopup);
     var popup = document.getElementById("thepopup");
     popup.parentNode.removeChild(popup);
   }
 }
 
 ];
+
+function platformIsMac()
+{
+    return navigator.platform.indexOf("Mac") > -1;
+}
--- a/toolkit/content/tests/chrome/test_bug624329.xul
+++ b/toolkit/content/tests/chrome/test_bug624329.xul
@@ -98,18 +98,33 @@ function openContextMenu() {
     }
 
     function checkPosition() {
         var menubox = menu.boxObject;
         var winbox = win.document.documentElement.boxObject
 
         var x = menubox.screenX - winbox.screenX;
         var y = menubox.screenY - winbox.screenY;
-        ok(y >= mouseY,
-           "menu top " + y + " should be below click point " + mouseY);
+
+        if (navigator.userAgent.indexOf("Mac") > -1)
+        {
+          // This check is alterered slightly for OSX which adds padding to the top
+          // and bottom of its context menus. The menu position calculation must
+          // be changed to allow for the pointer to be outside this padding
+          // when the menu opens.
+          // (Bug 1075089)
+          ok(y + 6 >= mouseY,
+             "menu top " + (y + 6) + " should be below click point " + mouseY);
+        }
+        else
+        {
+          ok(y >= mouseY,
+             "menu top " + y + " should be below click point " + mouseY);
+        }
+        
         ok(y <= mouseY + 20,
            "menu top " + y + " should not be too far below click point " + mouseY);
 
         ok(x < mouseX,
            "menu left " + x + " should be left of click point " + mouseX);
         var right = x + menubox.width;
         ok(right > mouseX,
            "menu right " + right + " should be right of click point " + mouseX);
--- a/toolkit/content/tests/chrome/test_contextmenu_list.xul
+++ b/toolkit/content/tests/chrome/test_contextmenu_list.xul
@@ -184,17 +184,17 @@ function checkContextMenu(event)
   isRoundedX(event.clientX, left, gTestElement + " clientX " + gSelectionStep + " " + gTestId + "," + frombase);
   isRoundedY(event.clientY, top, gTestElement + " clientY " + gSelectionStep + " " + gTestId);
   ok(event.screenX > left, gTestElement + " screenX " + gSelectionStep + " " + gTestId);
   ok(event.screenY > top, gTestElement + " screenY " + gSelectionStep + " " + gTestId);
 
   // context menu from mouse click
   switch (gTestId) {
     case -1:
-      var expected = gSelectionStep == 2 ? 1 : (navigator.platform.indexOf("Mac") >= 0 ? 3 : 0);
+      var expected = gSelectionStep == 2 ? 1 : (platformIsMac() ? 3 : 0);
       is($(gTestElement).selectedIndex, expected, "index after click " + gSelectionStep);
       break;
     case 0:
       if (gTestElement == "list")
         is(event.originalTarget, $("item3"), "list selection target");
       else
         is(event.originalTarget, $("treechildren"), "tree selection target");
       break;
@@ -217,57 +217,67 @@ function checkContextMenuForMenu(event)
   // but not when loaded separately) so just check for both cases for now
   ok(event.clientY == Math.round(popuprect.bottom) ||
      event.clientY - 1 == Math.round(popuprect.bottom), "menu top " + gSelectionStep);
 }
 
 function checkPopup()
 {
   var menurect = $("themenu").getBoundingClientRect();
+  
+  // Context menus are offset by a number of pixels from the mouse click
+  // which activates them. This is so that they don't appear exactly
+  // under the mouse which can cause them to be mistakenly dismissed.
+  // The number of pixels depends on the platform  and is defined in
+  // each platform's nsLookAndFeel
+  var contextMenuOffsetX = platformIsMac() ? 1 : 2;
+  var contextMenuOffsetY = platformIsMac() ? -6 : 2;
 
   if (gTestId == 0) {
     if (gTestElement == "list") {
       var itemrect = $("item3").getBoundingClientRect();
-      isRoundedX(menurect.left, itemrect.left + 2,
+      isRoundedX(menurect.left, itemrect.left + contextMenuOffsetX,
          "list selection keyboard left");
-      isRoundedY(menurect.top, itemrect.bottom + 2,
+      isRoundedY(menurect.top, itemrect.bottom + contextMenuOffsetY,
          "list selection keyboard top");
     }
     else {
       var tree = $("tree");
       var bodyrect = $("treechildren").getBoundingClientRect();
-      isRoundedX(menurect.left, bodyrect.left + 2,
+      isRoundedX(menurect.left, bodyrect.left + contextMenuOffsetX,
          "tree selection keyboard left");
       isRoundedY(menurect.top, bodyrect.top +
-         tree.treeBoxObject.rowHeight * 3 + 2,
+         tree.treeBoxObject.rowHeight * 3 + contextMenuOffsetY,
          "tree selection keyboard top");
     }
   }
   else if (gTestId == 1) {
-    // activating a context menu with the mouse from position (7, 1).
-    // Add 2 pixels to these values as context menus are offset by 2 pixels
-    // so that they don't appear exactly only the menu making them easier to
-    // dismiss. See nsXULPopupListener.
+    // activating a context menu with the mouse from position (7, 4).
     var elementrect = $(gTestElement).getBoundingClientRect();
-    isRoundedX(menurect.left, elementrect.left + 9,
+    isRoundedX(menurect.left, elementrect.left + 7 + contextMenuOffsetX,
        gTestElement + " mouse left");
-    isRoundedY(menurect.top, elementrect.top + 6,
+    isRoundedY(menurect.top, elementrect.top + 4 + contextMenuOffsetY,
        gTestElement + " mouse top");
   }
   else {
     var elementrect = $(gTestElement).getBoundingClientRect();
-    isRoundedX(menurect.left, elementrect.left + 2,
+    isRoundedX(menurect.left, elementrect.left + contextMenuOffsetX,
        gTestElement + " no selection keyboard left");
-    isRoundedY(menurect.top, elementrect.bottom + 2,
+    isRoundedY(menurect.top, elementrect.bottom + contextMenuOffsetY,
        gTestElement + " no selection keyboard top");
   }
 
   $("themenu").hidePopup();
 }
 
+function platformIsMac()
+{
+  return navigator.platform.indexOf("Mac") > -1;
+}
+
 ]]>
 </script>
 
 <body xmlns="http://www.w3.org/1999/xhtml">
 <p id="display">
 </p>
 <div id="content" style="display: none">
 </div>
--- a/toolkit/content/tests/chrome/window_largemenu.xul
+++ b/toolkit/content/tests/chrome/window_largemenu.xul
@@ -209,24 +209,27 @@ function popupHidden()
   }
 }
 
 function contextMenuPopupShown()
 {
   var popup = document.getElementById("popup");
   var rect = popup.getBoundingClientRect();
   var labelrect = document.getElementById("label").getBoundingClientRect();
-
-  is(rect.left, labelrect.left + 6, gTests[gTestIndex] + " left");
+  
+  // Click to open popup in popupHidden() occurs at (4,4) in label's coordinate space
+  var clickX = clickY = 4;
+  
+  is(rect.left, labelrect.left + clickX + (platformIsMac() ? 1 : 2), gTests[gTestIndex] + " left");
   switch (gTests[gTestIndex]) {
     case "context menu enough space below":
-      is(rect.top, labelrect.top + 6, gTests[gTestIndex] + " top");
+      is(rect.top, labelrect.top + clickY + (platformIsMac() ? -6 : 2), gTests[gTestIndex] + " top");
       break;
     case "context menu more space above":
-      is(rect.top, labelrect.top - rect.height + 2, gTests[gTestIndex] + " top");
+      is(rect.top, labelrect.top + clickY - rect.height - (platformIsMac() ? 0 : 2), gTests[gTestIndex] + " top");
       break;
     case "context menu too big either side":
       [, gScreenY] = getScreenXY(document.documentElement);
       // compare against the available size as well as the total size, as some
       // platforms allow the menu to overlap os chrome and others do not
       var pos = (screen.availTop + screen.availHeight - rect.height) - gScreenY;
       var availPos = (screen.top + screen.height - rect.height) - gScreenY;
       ok(rect.top == pos || rect.top == availPos,
@@ -276,17 +279,17 @@ function testPopupMovement()
 {
   var button = document.getElementById("label");
   var isPanelTest = (gTests[gTestIndex] == "panel movement");
   var popup = document.getElementById(isPanelTest ? "panel" : "popup");
 
   var screenX, screenY, buttonScreenX, buttonScreenY;
   var rect = popup.getBoundingClientRect();
 
-  var overlapOSChrome = (navigator.platform.indexOf("Mac") == -1);
+  var overlapOSChrome = !platformIsMac();
   popup.moveTo(1, 1);
   [screenX, screenY] = getScreenXY(popup);
 
   var expectedx = 1, expectedy = 1;
   if (!isPanelTest && !overlapOSChrome) {
     if (screen.availLeft >= 1) expectedx = screen.availLeft;
     if (screen.availTop >= 1) expectedy = screen.availTop;
   }
@@ -340,16 +343,21 @@ function testPopupMovement()
   [screenX, screenY] = getScreenXY(popup);
   [buttonScreenX, buttonScreenY] = getScreenXY(button);
   is(screenX, buttonScreenX, gTests[gTestIndex] + " original x");
   is(screenY, buttonScreenY + button.getBoundingClientRect().height, gTests[gTestIndex] + " original y");
 
   popup.hidePopup();
 }
 
+function platformIsMac()
+{
+  return navigator.platform.indexOf("Mac") > -1;
+}
+
 window.opener.wrappedJSObject.SimpleTest.waitForFocus(runTests, window);
 
 ]]>
 </script>
 
 <button id="label" label="OK" context="popup"/>
 <menupopup id="popup" onpopupshown="popupShown();" onpopuphidden="popupHidden();"
                       onoverflow="gOverflowed = true" onunderflow="gUnderflowed = true;">
--- a/widget/LookAndFeel.h
+++ b/widget/LookAndFeel.h
@@ -399,17 +399,24 @@ public:
       * the mouse in a scrollable frame.
       */
      eIntID_ScrollbarDisplayOnMouseMove,
 
      /*
       * Overlay scrollbar animation constants.
       */
      eIntID_ScrollbarFadeBeginDelay,
-     eIntID_ScrollbarFadeDuration
+     eIntID_ScrollbarFadeDuration,
+      
+     /**
+      * Distance in pixels to offset the context menu from the cursor
+      * on open.
+      */
+     eIntID_ContextMenuOffsetVertical,
+     eIntID_ContextMenuOffsetHorizontal
   };
 
   /**
    * Windows themes we currently detect.
    */
   enum WindowsTheme {
     eWindowsTheme_Generic = 0, // unrecognized theme
     eWindowsTheme_Classic,
--- a/widget/android/nsLookAndFeel.cpp
+++ b/widget/android/nsLookAndFeel.cpp
@@ -410,16 +410,21 @@ nsLookAndFeel::GetIntImpl(IntID aID, int
 
         case eIntID_SpellCheckerUnderlineStyle:
             aResult = NS_STYLE_TEXT_DECORATION_STYLE_WAVY;
             break;
 
         case eIntID_ScrollbarButtonAutoRepeatBehavior:
             aResult = 0;
             break;
+        
+        case eIntID_ContextMenuOffsetVertical:
+        case eIntID_ContextMenuOffsetHorizontal:
+            aResult = 2;
+            break;
 
         default:
             aResult = 0;
             rv = NS_ERROR_FAILURE;
     }
 
     return rv;
 }
--- a/widget/cocoa/nsLookAndFeel.mm
+++ b/widget/cocoa/nsLookAndFeel.mm
@@ -468,16 +468,22 @@ nsLookAndFeel::GetIntImpl(IntID aID, int
       if ([NSEvent respondsToSelector:@selector(
             isSwipeTrackingFromScrollEventsEnabled)]) {
         aResult = [NSEvent isSwipeTrackingFromScrollEventsEnabled] ? 1 : 0;
       }
       break;
     case eIntID_ColorPickerAvailable:
       aResult = 1;
       break;
+    case eIntID_ContextMenuOffsetVertical:
+      aResult = -6;
+      break;
+    case eIntID_ContextMenuOffsetHorizontal:
+      aResult = 1;
+      break;
     default:
       aResult = 0;
       res = NS_ERROR_FAILURE;
   }
   return res;
 
   NS_OBJC_END_TRY_ABORT_BLOCK_NSRESULT;
 }
--- a/widget/gonk/nsLookAndFeel.cpp
+++ b/widget/gonk/nsLookAndFeel.cpp
@@ -387,16 +387,21 @@ nsLookAndFeel::GetIntImpl(IntID aID, int
             break;
 
         case eIntID_PhysicalHomeButton: {
             char propValue[PROPERTY_VALUE_MAX];
             property_get("ro.moz.has_home_button", propValue, "1");
             aResult = atoi(propValue);
             break;
         }
+            
+        case eIntID_ContextMenuOffsetVertical:
+        case eIntID_ContextMenuOffsetHorizontal:
+            aResult = 2;
+            break;
 
         default:
             aResult = 0;
             rv = NS_ERROR_FAILURE;
     }
 
     return rv;
 }
--- a/widget/gtk/nsLookAndFeel.cpp
+++ b/widget/gtk/nsLookAndFeel.cpp
@@ -662,16 +662,20 @@ nsLookAndFeel::GetIntImpl(IntID aID, int
         aResult = 1;
         break;
     case eIntID_SwipeAnimationEnabled:
         aResult = 0;
         break;
     case eIntID_ColorPickerAvailable:
         aResult = 1;
         break;
+    case eIntID_ContextMenuOffsetVertical:
+    case eIntID_ContextMenuOffsetHorizontal:
+        aResult = 2;
+        break;
     default:
         aResult = 0;
         res     = NS_ERROR_FAILURE;
     }
 
     return res;
 }
 
--- a/widget/nsXPLookAndFeel.cpp
+++ b/widget/nsXPLookAndFeel.cpp
@@ -112,16 +112,22 @@ nsLookAndFeelIntPref nsXPLookAndFeel::sI
     eIntID_ScrollbarButtonAutoRepeatBehavior,
     false, 0 },
   { "ui.tooltipDelay",
     eIntID_TooltipDelay,
     false, 0 },
   { "ui.physicalHomeButton",
     eIntID_PhysicalHomeButton,
     false, 0 },
+  { "ui.contextMenuOffsetVertical",
+    eIntID_ContextMenuOffsetVertical,
+    false, 0 },
+  { "ui.contextMenuOffsetHorizontal",
+    eIntID_ContextMenuOffsetHorizontal,
+    false, 0 }
 };
 
 nsLookAndFeelFloatPref nsXPLookAndFeel::sFloatPrefs[] =
 {
   { "ui.IMEUnderlineRelativeSize",
     eFloatID_IMEUnderlineRelativeSize,
     false, 0 },
   { "ui.SpellCheckerUnderlineRelativeSize",
--- a/widget/qt/nsLookAndFeel.cpp
+++ b/widget/qt/nsLookAndFeel.cpp
@@ -368,16 +368,21 @@ nsLookAndFeel::GetIntImpl(IntID aID, int
 
         case eIntID_SpellCheckerUnderlineStyle:
             aResult = NS_STYLE_TEXT_DECORATION_STYLE_WAVY;
             break;
 
         case eIntID_ScrollbarButtonAutoRepeatBehavior:
             aResult = 0;
             break;
+        
+        case eIntID_ContextMenuOffsetVertical:
+        case eIntID_ContextMenuOffsetHorizontal:
+            aResult = 2;
+            break;
 
         default:
             aResult = 0;
             rv = NS_ERROR_FAILURE;
     }
 
     return rv;
 }
--- a/widget/uikit/nsLookAndFeel.mm
+++ b/widget/uikit/nsLookAndFeel.mm
@@ -340,16 +340,20 @@ nsLookAndFeel::GetIntImpl(IntID aID, int
     case eIntID_IMEConvertedTextUnderlineStyle:
     case eIntID_IMESelectedRawTextUnderlineStyle:
     case eIntID_IMESelectedConvertedTextUnderline:
       aResult = NS_STYLE_TEXT_DECORATION_STYLE_SOLID;
       break;
     case eIntID_SpellCheckerUnderlineStyle:
       aResult = NS_STYLE_TEXT_DECORATION_STYLE_DOTTED;
       break;
+    case eIntID_ContextMenuOffsetVertical:
+    case eIntID_ContextMenuOffsetHorizontal:
+      aResult = 2;
+      break;
     default:
       aResult = 0;
       res = NS_ERROR_FAILURE;
   }
   return res;
 }
 
 NS_IMETHODIMP
--- a/widget/windows/nsLookAndFeel.cpp
+++ b/widget/windows/nsLookAndFeel.cpp
@@ -482,16 +482,20 @@ nsLookAndFeel::GetIntImpl(IntID aID, int
         aResult = 1;
         break;
     case eIntID_ScrollbarFadeBeginDelay:
         aResult = 2500;
         break;
     case eIntID_ScrollbarFadeDuration:
         aResult = 350;
         break;
+    case eIntID_ContextMenuOffsetVertical:
+    case eIntID_ContextMenuOffsetHorizontal:
+        aResult = 2;
+        break;
     default:
         aResult = 0;
         res = NS_ERROR_FAILURE;
     }
   return res;
 }
 
 nsresult