Bug 350471 - Reenable pixel scrolling (two-finger touchpad), r=smaug r=smichaud sr=roc
authorMarkus Stange <mstange@themasta.com>
Wed, 17 Sep 2008 13:27:19 +0200
changeset 19319 93f23e3efbb4
parent 19318 084411a8f097
child 19320 0561cb13ec86
push id2124
push usermstange@themasta.com
push date2008-09-17 11:28 +0000
treeherdermozilla-central@93f23e3efbb4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
reviewerssmaug, smichaud, roc
bugs350471
milestone1.9.1b1pre
Bug 350471 - Reenable pixel scrolling (two-finger touchpad), r=smaug r=smichaud sr=roc
content/base/src/nsContentUtils.cpp
content/base/src/nsGkAtomList.h
content/events/src/nsDOMEvent.cpp
content/events/src/nsDOMEvent.h
content/events/src/nsEventStateManager.cpp
content/events/src/nsEventStateManager.h
content/events/test/Makefile.in
content/events/test/test_bug350471.xul
dom/public/idl/base/nsIDOMWindowUtils.idl
dom/src/base/nsDOMWindowUtils.cpp
modules/libpref/src/init/all.js
testing/mochitest/tests/SimpleTest/EventUtils.js
toolkit/content/tests/widgets/test_mousescroll.xul
toolkit/content/tests/widgets/tree_shared.js
widget/public/nsGUIEvent.h
widget/src/cocoa/nsChildView.h
widget/src/cocoa/nsChildView.mm
--- a/content/base/src/nsContentUtils.cpp
+++ b/content/base/src/nsContentUtils.cpp
@@ -405,16 +405,17 @@ nsContentUtils::InitializeEventTable() {
     { &nsGkAtoms::onDOMNodeRemoved,              { NS_MUTATION_NODEREMOVED, EventNameType_HTMLXUL }},
     { &nsGkAtoms::onDOMNodeInsertedIntoDocument, { NS_MUTATION_NODEINSERTEDINTODOCUMENT, EventNameType_HTMLXUL }},
     { &nsGkAtoms::onDOMNodeRemovedFromDocument,  { NS_MUTATION_NODEREMOVEDFROMDOCUMENT, EventNameType_HTMLXUL }},
     { &nsGkAtoms::onDOMSubtreeModified,          { NS_MUTATION_SUBTREEMODIFIED, EventNameType_HTMLXUL }},
     { &nsGkAtoms::onDOMActivate,                 { NS_UI_ACTIVATE, EventNameType_HTMLXUL }},
     { &nsGkAtoms::onDOMFocusIn,                  { NS_UI_FOCUSIN, EventNameType_HTMLXUL }},
     { &nsGkAtoms::onDOMFocusOut,                 { NS_UI_FOCUSOUT, EventNameType_HTMLXUL }},
     { &nsGkAtoms::onDOMMouseScroll,              { NS_MOUSE_SCROLL, EventNameType_HTMLXUL }},
+    { &nsGkAtoms::onMozMousePixelScroll,         { NS_MOUSE_PIXEL_SCROLL, EventNameType_HTMLXUL }},
     { &nsGkAtoms::oninput,                       { NS_FORM_INPUT, EventNameType_HTMLXUL }},
     { &nsGkAtoms::onpageshow,                    { NS_PAGE_SHOW, EventNameType_HTML }},
     { &nsGkAtoms::onpagehide,                    { NS_PAGE_HIDE, EventNameType_HTML }},
     { &nsGkAtoms::onresize,                      { NS_RESIZE_EVENT,
                                                  (EventNameType_HTMLXUL | EventNameType_SVGSVG) }},
     { &nsGkAtoms::onscroll,                      { NS_SCROLL_EVENT,
                                                  (EventNameType_HTMLXUL | EventNameType_SVGSVG) }},
     { &nsGkAtoms::oncopy,                        { NS_COPY, EventNameType_HTMLXUL }},
--- a/content/base/src/nsGkAtomList.h
+++ b/content/base/src/nsGkAtomList.h
@@ -636,16 +636,17 @@ GK_ATOM(onkeyup, "onkeyup")
 GK_ATOM(onLoad, "onLoad")
 GK_ATOM(onload, "onload")
 GK_ATOM(only, "only")               // this one is not an event
 GK_ATOM(onmousedown, "onmousedown")
 GK_ATOM(onmousemove, "onmousemove")
 GK_ATOM(onmouseout, "onmouseout")
 GK_ATOM(onmouseover, "onmouseover")
 GK_ATOM(onmouseup, "onmouseup")
+GK_ATOM(onMozMousePixelScroll, "onMozMousePixelScroll")
 GK_ATOM(ononline, "ononline")
 GK_ATOM(onoffline, "onoffline")
 GK_ATOM(onoverflow, "onoverflow")
 GK_ATOM(onoverflowchanged, "onoverflowchanged")
 GK_ATOM(onpagehide, "onpagehide")
 GK_ATOM(onpageshow, "onpageshow")
 GK_ATOM(onpaint, "onpaint")
 GK_ATOM(onpaste, "onpaste")
--- a/content/events/src/nsDOMEvent.cpp
+++ b/content/events/src/nsDOMEvent.cpp
@@ -65,18 +65,18 @@ static const char* const sEventNames[] =
   "popuphiding", "popuphidden", "close", "command", "broadcast", "commandupdate",
   "dragenter", "dragover", "dragexit", "dragdrop", "draggesture",
   "drag", "dragend", "dragstart", "dragleave", "drop", "resize",
   "scroll", "overflow", "underflow", "overflowchanged",
   "DOMSubtreeModified", "DOMNodeInserted", "DOMNodeRemoved", 
   "DOMNodeRemovedFromDocument", "DOMNodeInsertedIntoDocument",
   "DOMAttrModified", "DOMCharacterDataModified",
   "DOMActivate", "DOMFocusIn", "DOMFocusOut",
-  "pageshow", "pagehide", "DOMMouseScroll", "offline", "online",
-  "copy", "cut", "paste"
+  "pageshow", "pagehide", "DOMMouseScroll", "MozMousePixelScroll",
+  "offline", "online", "copy", "cut", "paste"
 #ifdef MOZ_SVG
  ,
   "SVGLoad", "SVGUnload", "SVGAbort", "SVGError", "SVGResize", "SVGScroll",
   "SVGZoom"
 #endif // MOZ_SVG
 #ifdef MOZ_MEDIA
   ,
   "loadstart", "progress", "loadedmetadata", "loadedfirstframe",
@@ -474,16 +474,18 @@ nsDOMEvent::SetEventType(const nsAString
       mEvent->message = NS_MOUSE_EXIT_SYNTH;
     else if (atom == nsGkAtoms::onmousemove)
       mEvent->message = NS_MOUSE_MOVE;
     else if (atom == nsGkAtoms::oncontextmenu)
       mEvent->message = NS_CONTEXTMENU;
   } else if (mEvent->eventStructType == NS_MOUSE_SCROLL_EVENT) {
     if (atom == nsGkAtoms::onDOMMouseScroll)
       mEvent->message = NS_MOUSE_SCROLL;
+    else if (atom == nsGkAtoms::onMozMousePixelScroll)
+      mEvent->message = NS_MOUSE_PIXEL_SCROLL;
   } else if (mEvent->eventStructType == NS_DRAG_EVENT) {
     if (atom == nsGkAtoms::ondragstart)
       mEvent->message = NS_DRAGDROP_START;
     else if (atom == nsGkAtoms::ondraggesture)
       mEvent->message = NS_DRAGDROP_GESTURE;
     else if (atom == nsGkAtoms::ondragenter)
       mEvent->message = NS_DRAGDROP_ENTER;
     else if (atom == nsGkAtoms::ondragover)
@@ -1388,16 +1390,18 @@ const char* nsDOMEvent::GetEventName(PRU
   case NS_UI_FOCUSOUT:
     return sEventNames[eDOMEvents_DOMFocusOut];
   case NS_PAGE_SHOW:
     return sEventNames[eDOMEvents_pageshow];
   case NS_PAGE_HIDE:
     return sEventNames[eDOMEvents_pagehide];
   case NS_MOUSE_SCROLL:
     return sEventNames[eDOMEvents_DOMMouseScroll];
+  case NS_MOUSE_PIXEL_SCROLL:
+    return sEventNames[eDOMEvents_MozMousePixelScroll];
   case NS_OFFLINE:
     return sEventNames[eDOMEvents_offline];
   case NS_ONLINE:
     return sEventNames[eDOMEvents_online];
   case NS_COPY:
     return sEventNames[eDOMEvents_copy];
   case NS_CUT:
     return sEventNames[eDOMEvents_cut];
--- a/content/events/src/nsDOMEvent.h
+++ b/content/events/src/nsDOMEvent.h
@@ -119,16 +119,17 @@ public:
     eDOMEvents_attrmodified,
     eDOMEvents_characterdatamodified,
     eDOMEvents_DOMActivate,
     eDOMEvents_DOMFocusIn,
     eDOMEvents_DOMFocusOut,
     eDOMEvents_pageshow,
     eDOMEvents_pagehide,
     eDOMEvents_DOMMouseScroll,
+    eDOMEvents_MozMousePixelScroll,
     eDOMEvents_offline,
     eDOMEvents_online,
     eDOMEvents_copy,
     eDOMEvents_cut,
     eDOMEvents_paste
 #ifdef MOZ_SVG
    ,
     eDOMEvents_SVGLoad,
--- a/content/events/src/nsEventStateManager.cpp
+++ b/content/events/src/nsEventStateManager.cpp
@@ -108,17 +108,17 @@
 #include "nsIDOMDocument.h"
 #include "nsIDOMKeyEvent.h"
 #include "nsIObserverService.h"
 #include "nsIDocShell.h"
 #include "nsIMarkupDocumentViewer.h"
 #include "nsIScrollableViewProvider.h"
 #include "nsIDOMDocumentRange.h"
 #include "nsIDOMDocumentEvent.h"
-#include "nsIDOMMouseEvent.h"
+#include "nsIDOMMouseScrollEvent.h"
 #include "nsIDOMDragEvent.h"
 #include "nsIDOMEventTarget.h"
 #include "nsIDOMDocumentView.h"
 #include "nsIDOMNSUIEvent.h"
 #include "nsDOMDragEvent.h"
 
 #include "nsIDOMRange.h"
 #include "nsCaret.h"
@@ -136,16 +136,17 @@
 #include "imgIContainer.h"
 #include "nsIProperties.h"
 #include "nsISupportsPrimitives.h"
 #include "nsEventDispatcher.h"
 #include "nsPresShellIterator.h"
 
 #include "nsServiceManagerUtils.h"
 #include "nsITimer.h"
+#include "nsIFontMetrics.h"
 
 #include "nsIDragService.h"
 #include "nsIDragSession.h"
 #include "nsDOMDataTransfer.h"
 #include "nsContentAreaDragDrop.h"
 #ifdef MOZ_XUL
 #include "nsITreeBoxObject.h"
 #endif
@@ -455,17 +456,19 @@ nsEventStateManager::nsEventStateManager
     mLastFocusedWith(eEventFocusedByUnknown),
     mPresContext(nsnull),
     mLClickCount(0),
     mMClickCount(0),
     mRClickCount(0),
     mNormalLMouseEventInProcess(PR_FALSE),
     m_haveShutdown(PR_FALSE),
     mBrowseWithCaret(PR_FALSE),
-    mTabbedThroughDocument(PR_FALSE)
+    mTabbedThroughDocument(PR_FALSE),
+    mLastLineScrollConsumedX(PR_FALSE),
+    mLastLineScrollConsumedY(PR_FALSE)
 {
   if (sESMInstanceCount == 0) {
     gUserInteractionTimerCallback = new nsUITimerCallback();
     if (gUserInteractionTimerCallback) {
       NS_ADDREF(gUserInteractionTimerCallback);
       CallCreateInstance("@mozilla.org/timer;1", &gUserInteractionTimer);
       if (gUserInteractionTimer) {
         gUserInteractionTimer->InitWithCallback(gUserInteractionTimerCallback,
@@ -1402,16 +1405,30 @@ nsEventStateManager::PreHandleEvent(nsPr
            (msEvent->scrollFlags & nsMouseScrollEvent::kIsFullPage)) ||
           action == MOUSE_SCROLL_PAGE) {
           msEvent->delta = (msEvent->delta > 0)
             ? nsIDOMNSUIEvent::SCROLL_PAGE_DOWN
             : nsIDOMNSUIEvent::SCROLL_PAGE_UP;
       }
     }
     break;
+  case NS_MOUSE_PIXEL_SCROLL:
+    {
+      if (mCurrentFocus) {
+        mCurrentTargetContent = mCurrentFocus;
+      }
+
+      // When the last line scroll has been canceled, eat the pixel scroll event
+      nsMouseScrollEvent *msEvent = static_cast<nsMouseScrollEvent*>(aEvent);
+      if ((msEvent->scrollFlags & nsMouseScrollEvent::kIsHorizontal) ?
+           mLastLineScrollConsumedX : mLastLineScrollConsumedY) {
+        *aStatus = nsEventStatus_eConsumeNoDefault;
+      }
+    }
+    break;
   case NS_QUERY_SELECTED_TEXT:
     {
       nsQueryContentEventHandler handler(mPresContext);
       handler.OnQuerySelectedText((nsQueryContentEvent*)aEvent);
     }
     break;
   case NS_QUERY_TEXT_CONTENT:
     {
@@ -2409,16 +2426,76 @@ GetParentFrameToScroll(nsPresContext* aP
     return nsnull;
 
   if (aFrame->GetStyleDisplay()->mPosition == NS_STYLE_POSITION_FIXED)
     return aPresContext->GetPresShell()->GetRootScrollFrame();
 
   return aFrame->GetParent();
 }
 
+static nsIScrollableView*
+GetScrollableViewForFrame(nsPresContext* aPresContext, nsIFrame* aFrame)
+{
+  for (; aFrame; aFrame = GetParentFrameToScroll(aPresContext, aFrame)) {
+    nsIScrollableViewProvider* svp;
+    CallQueryInterface(aFrame, &svp);
+    if (svp) {
+      nsIScrollableView* scrollView = svp->GetScrollableView();
+      if (scrollView)
+        return scrollView;
+    }
+  }
+  return nsnull;
+}
+
+void
+nsEventStateManager::SendPixelScrollEvent(nsIFrame* aTargetFrame,
+                                          nsMouseScrollEvent* aEvent,
+                                          nsPresContext* aPresContext,
+                                          nsEventStatus* aStatus)
+{
+  nsCOMPtr<nsIContent> targetContent = aTargetFrame->GetContent();
+  if (!targetContent)
+    GetFocusedContent(getter_AddRefs(targetContent));
+  if (!targetContent)
+    return;
+
+  while (targetContent->IsNodeOfType(nsINode::eTEXT)) {
+    targetContent = targetContent->GetParent();
+  }
+
+  nsIScrollableView* scrollView = GetScrollableViewForFrame(aPresContext, aTargetFrame);
+  nscoord lineHeight = 0;
+  if (scrollView) {
+    scrollView->GetLineHeight(&lineHeight);
+  } else {
+    // Fall back to the font height of the target frame.
+    const nsStyleFont* font = aTargetFrame->GetStyleFont();
+    const nsFont& f = font->mFont;
+    nsCOMPtr<nsIFontMetrics> fm = aPresContext->GetMetricsFor(f);
+    NS_ASSERTION(fm, "FontMetrics is null!");
+    if (fm)
+      fm->GetHeight(lineHeight);
+  }
+
+  PRBool isTrusted = (aEvent->flags & NS_EVENT_FLAG_TRUSTED) != 0;
+  nsMouseScrollEvent event(isTrusted, NS_MOUSE_PIXEL_SCROLL, nsnull);
+  event.refPoint = aEvent->refPoint;
+  event.widget = aEvent->widget;
+  event.time = aEvent->time;
+  event.isShift = aEvent->isShift;
+  event.isControl = aEvent->isControl;
+  event.isAlt = aEvent->isAlt;
+  event.isMeta = aEvent->isMeta;
+  event.scrollFlags = aEvent->scrollFlags;
+  event.delta = aPresContext->AppUnitsToIntCSSPixels(aEvent->delta * lineHeight);
+
+  nsEventDispatcher::Dispatch(targetContent, aPresContext, &event, nsnull, aStatus);
+}
+
 nsresult
 nsEventStateManager::DoScrollText(nsPresContext* aPresContext,
                                   nsIFrame* aTargetFrame,
                                   nsInputEvent* aEvent,
                                   PRInt32 aNumLines,
                                   PRBool aScrollHorizontal,
                                   ScrollQuantity aScrollQuantity)
 {
@@ -2727,89 +2804,116 @@ nsEventStateManager::PostHandleEvent(nsP
           }
         }
         nsCOMPtr<nsFrameSelection> frameSelection = shell->FrameSelection();
         frameSelection->SetMouseDownState(PR_FALSE);
       }
     }
     break;
   case NS_MOUSE_SCROLL:
-    if (nsEventStatus_eConsumeNoDefault != *aStatus) {
-
-      // Build the preference keys, based on the event properties.
-      nsMouseScrollEvent *msEvent = (nsMouseScrollEvent*) aEvent;
-
-      NS_NAMED_LITERAL_CSTRING(actionslot,      ".action");
-      NS_NAMED_LITERAL_CSTRING(sysnumlinesslot, ".sysnumlines");
-
-      nsCAutoString baseKey;
-      GetBasePrefKeyForMouseWheel(msEvent, baseKey);
-
-      // Extract the preferences
-      nsCAutoString actionKey(baseKey);
-      actionKey.Append(actionslot);
-
-      nsCAutoString sysNumLinesKey(baseKey);
-      sysNumLinesKey.Append(sysnumlinesslot);
-
-      PRInt32 action = nsContentUtils::GetIntPref(actionKey.get());
-      PRBool useSysNumLines =
-        nsContentUtils::GetBoolPref(sysNumLinesKey.get());
-
-      if (useSysNumLines) {
-        if (msEvent->scrollFlags & nsMouseScrollEvent::kIsFullPage)
-          action = MOUSE_SCROLL_PAGE;
-        else if (msEvent->scrollFlags & nsMouseScrollEvent::kIsPixels)
-          action = MOUSE_SCROLL_PIXELS;
-      }
-
-      switch (action) {
-      case MOUSE_SCROLL_N_LINES:
-        {
-          DoScrollText(presContext, aTargetFrame, msEvent, msEvent->delta,
-                       (msEvent->scrollFlags & nsMouseScrollEvent::kIsHorizontal),
-                       eScrollByLine);
+  case NS_MOUSE_PIXEL_SCROLL:
+    {
+      nsMouseScrollEvent *msEvent = static_cast<nsMouseScrollEvent*>(aEvent);
+
+      if (aEvent->message == NS_MOUSE_SCROLL) {
+        // Mark the subsequent pixel scrolls as valid / invalid, based on the
+        // observation if the previous line scroll has been canceled
+        if (msEvent->scrollFlags & nsMouseScrollEvent::kIsHorizontal) {
+          mLastLineScrollConsumedX = (nsEventStatus_eConsumeNoDefault == *aStatus);
+        } else if (msEvent->scrollFlags & nsMouseScrollEvent::kIsVertical) {
+          mLastLineScrollConsumedY = (nsEventStatus_eConsumeNoDefault == *aStatus);
         }
-        break;
-
-      case MOUSE_SCROLL_PAGE:
-        {
-          DoScrollText(presContext, aTargetFrame, msEvent, msEvent->delta,
-                       (msEvent->scrollFlags & nsMouseScrollEvent::kIsHorizontal),
-                       eScrollByPage);
-        }
-        break;
-
-      case MOUSE_SCROLL_PIXELS:
-        {
-          DoScrollText(presContext, aTargetFrame, msEvent, msEvent->delta,
-                       (msEvent->scrollFlags & nsMouseScrollEvent::kIsHorizontal),
-                       eScrollByPixel);
+        if (!(msEvent->scrollFlags & nsMouseScrollEvent::kHasPixels)) {
+          // No generated pixel scroll event will follow.
+          // Create and send a pixel scroll DOM event now.
+          SendPixelScrollEvent(aTargetFrame, msEvent, presContext, aStatus);
         }
-        break;
-
-      case MOUSE_SCROLL_HISTORY:
-        {
-          DoScrollHistory(msEvent->delta);
+      }
+
+      if (*aStatus != nsEventStatus_eConsumeNoDefault) {
+        // Build the preference keys, based on the event properties.
+        NS_NAMED_LITERAL_CSTRING(actionslot,      ".action");
+        NS_NAMED_LITERAL_CSTRING(sysnumlinesslot, ".sysnumlines");
+
+        nsCAutoString baseKey;
+        GetBasePrefKeyForMouseWheel(msEvent, baseKey);
+
+        // Extract the preferences
+        nsCAutoString actionKey(baseKey);
+        actionKey.Append(actionslot);
+
+        nsCAutoString sysNumLinesKey(baseKey);
+        sysNumLinesKey.Append(sysnumlinesslot);
+
+        PRInt32 action = nsContentUtils::GetIntPref(actionKey.get());
+        PRBool useSysNumLines =
+          nsContentUtils::GetBoolPref(sysNumLinesKey.get());
+
+        if (useSysNumLines) {
+          if (msEvent->scrollFlags & nsMouseScrollEvent::kIsFullPage)
+            action = MOUSE_SCROLL_PAGE;
         }
-        break;
-
-      case MOUSE_SCROLL_ZOOM:
-        {
-          DoScrollZoom(aTargetFrame, msEvent->delta);
+
+        if (aEvent->message == NS_MOUSE_PIXEL_SCROLL) {
+          if (action == MOUSE_SCROLL_N_LINES) {
+             action = MOUSE_SCROLL_PIXELS;
+          } else {
+            // Do not scroll pixels when zooming
+            action = -1;
+          }
+        } else if (msEvent->scrollFlags & nsMouseScrollEvent::kHasPixels) {
+          if (action == MOUSE_SCROLL_N_LINES) {
+            // We shouldn't scroll lines when a pixel scroll event will follow.
+            action = -1;
+          }
         }
-        break;
-
-      default:  // Including -1 (do nothing)
-        break;
+
+        switch (action) {
+        case MOUSE_SCROLL_N_LINES:
+          {
+            DoScrollText(presContext, aTargetFrame, msEvent, msEvent->delta,
+                         (msEvent->scrollFlags & nsMouseScrollEvent::kIsHorizontal),
+                         eScrollByLine);
+          }
+          break;
+
+        case MOUSE_SCROLL_PAGE:
+          {
+            DoScrollText(presContext, aTargetFrame, msEvent, msEvent->delta,
+                         (msEvent->scrollFlags & nsMouseScrollEvent::kIsHorizontal),
+                         eScrollByPage);
+          }
+          break;
+
+        case MOUSE_SCROLL_PIXELS:
+          {
+            DoScrollText(presContext, aTargetFrame, msEvent, msEvent->delta,
+                         (msEvent->scrollFlags & nsMouseScrollEvent::kIsHorizontal),
+                         eScrollByPixel);
+          }
+          break;
+
+        case MOUSE_SCROLL_HISTORY:
+          {
+            DoScrollHistory(msEvent->delta);
+          }
+          break;
+
+        case MOUSE_SCROLL_ZOOM:
+          {
+            DoScrollZoom(aTargetFrame, msEvent->delta);
+          }
+          break;
+
+        default:  // Including -1 (do nothing)
+          break;
+        }
+        *aStatus = nsEventStatus_eConsumeNoDefault;
       }
-      *aStatus = nsEventStatus_eConsumeNoDefault;
-
     }
-
     break;
 
   case NS_DRAGDROP_ENTER:
   case NS_DRAGDROP_OVER:
     {
       NS_ASSERTION(aEvent->eventStructType == NS_DRAG_EVENT, "Expected a drag event");
 
       nsCOMPtr<nsIDragSession> dragSession = nsContentUtils::GetDragSession();
--- a/content/events/src/nsEventStateManager.h
+++ b/content/events/src/nsEventStateManager.h
@@ -290,16 +290,20 @@ protected:
                        nsIDocShellTreeItem** aResult);
 
   // These functions are for mousewheel scrolling
   nsresult GetParentScrollingView(nsInputEvent* aEvent,
                                   nsPresContext* aPresContext,
                                   nsIFrame* &targetOuterFrame,
                                   nsPresContext* &presCtxOuter);
 
+  void SendPixelScrollEvent(nsIFrame* aTargetFrame,
+                            nsMouseScrollEvent* aEvent,
+                            nsPresContext* aPresContext,
+                            nsEventStatus* aStatus);
   typedef enum {
     eScrollByPixel,
     eScrollByLine,
     eScrollByPage
   } ScrollQuantity;
   nsresult DoScrollText(nsPresContext* aPresContext,
                         nsIFrame* aTargetFrame,
                         nsInputEvent* aEvent,
@@ -440,16 +444,20 @@ protected:
   // Recursion guard for tabbing
   PRPackedBool mTabbedThroughDocument;
 
   // Array for accesskey support
   nsCOMArray<nsIContent> mAccessKeys;
 
   nsCOMArray<nsIDocShell> mTabbingFromDocShells;
 
+  // Unlocks pixel scrolling
+  PRPackedBool mLastLineScrollConsumedX;
+  PRPackedBool mLastLineScrollConsumedY;
+
 #ifdef CLICK_HOLD_CONTEXT_MENUS
   enum { kClickHoldDelay = 500 } ;        // 500ms == 1/2 second
 
   void CreateClickHoldTimer ( nsPresContext* aPresContext, nsIFrame* inDownFrame,
                               nsGUIEvent* inMouseDownEvent ) ;
   void KillClickHoldTimer ( ) ;
   void FireContextClick ( ) ;
   static void sClickHoldCallback ( nsITimer* aTimer, void* aESM ) ;
--- a/content/events/test/Makefile.in
+++ b/content/events/test/Makefile.in
@@ -46,16 +46,17 @@ include $(topsrcdir)/config/rules.mk
 
 _TEST_FILES = \
 		test_bug238987.html \
 		test_bug288392.html \
 		test_bug328885.html \
 		test_bug336682_1.html \
 		test_bug336682_2.xul \
 		test_bug336682.js \
+		test_bug350471.xul \
 		test_bug367781.html \
 		test_bug368835.html \
 		test_bug379120.html \
 		test_bug391568.xhtml \
 		test_bug402089.html \
 		test_bug405632.html \
 		test_bug409604.html \
 		test_bug412567.html \
new file mode 100644
--- /dev/null
+++ b/content/events/test/test_bug350471.xul
@@ -0,0 +1,229 @@
+<?xml version="1.0"?>
+<?xml-stylesheet href="chrome://global/skin" type="text/css"?>
+<?xml-stylesheet href="/tests/SimpleTest/test.css" type="text/css"?>
+<!--
+https://bugzilla.mozilla.org/show_bug.cgi?id=350471
+-->
+<window title="Mozilla Bug 350471"
+  xmlns="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
+
+  <title>Test for Bug 350471</title>
+  <script type="application/javascript" src="/MochiKit/packed.js" />
+  <script type="application/javascript" src="/tests/SimpleTest/SimpleTest.js"/>
+  <script type="application/javascript" src="/tests/SimpleTest/EventUtils.js"/>
+<body  xmlns="http://www.w3.org/1999/xhtml">
+  <a target="_blank" href="https://bugzilla.mozilla.org/show_bug.cgi?id=350471">Mozilla Bug 350471</a>
+
+  <p id="display"></p>
+<div id="content" style="display: none">
+</div>
+</body>
+
+<vbox style="height: 150px; background: cyan; overflow: auto;" id="scrollbox">
+  <hbox style="height: 8000px;"><vbox style="width: 8000px;"/></hbox>
+</vbox>
+
+<script class="testbody" type="application/javascript;version=1.7"><![CDATA[
+
+/** Test for Bug 350471 **/
+
+// This test depends on general.smoothScroll being off.
+
+const minLineHeight = 10, maxLineHeight = 20;
+
+function between(x, min, max) (min <= max) ? (min <= x && x <= max) : (max <= x && x <= min);
+function isbetween(x, min, max, msg) ok(between(x, min, max), msg + " - Expected " + min + " to " + max + ", got " + x);
+
+function testEventDispatching() {
+  function helper(aAxis, aDelta, aKind, aShiftKey, aCtrlKey, aAltKey, aMetaKey) {
+    let expectedEvents = [];
+    let deltaUnit = "";
+    function listener(e) {
+      if (!expectedEvents.length) {
+        ok(false, "Received an event that I didn't expect. type: " + e.type +
+           ", axis: " + e.axis + ", delta: " + e.delta);
+        return;
+      }
+      let expected = expectedEvents.shift();
+      
+      ["type", "shiftKey", "ctrlKey", "altKey", "metaKey"].forEach(function(field) {
+        is(e[field], expected[field],
+          "e." + field + " (" + e[field] + ") does not match expected value (" + expected[field] + ")");
+      });
+      
+      let expectedAxis = expected.axis == "horizontal" ? e.HORIZONTAL_AXIS : e.VERTICAL_AXIS;
+      is(e.axis, expectedAxis,
+         "e.axis (" + e.axis + ") does not match expected value (" + expectedAxis + ")");
+      
+      // When modifier keys are pressed, cancel the event.
+      // We don't want to zoom or navigate back / forward (history scroll).
+      if (aShiftKey || aCtrlKey || aAltKey || aMetaKey) {
+        e.preventDefault();
+        // Note: If this is a DOMMouseScroll event without hasPixels, we still
+        // expect a follow-up MozMousePixelScroll event.
+      } else {
+        // Only check the delta if no modifiers are pressed.
+        // History scroll and zoom change the deltas in nsESM::PreHandleEvent.
+        if (deltaUnit == (e.type == "DOMMouseScroll" ? "lines" : "pixels")) {
+          // no unit conversion necessary
+          is(e.detail, expected.delta,
+             "e.detail (" + e.detail + ") does not match expected value (" + expected.delta + ")");
+        } else if (e.type == "MozMousePixelScroll") {
+          // We sent a line scroll event but are receiving a pixel scroll event,
+          // so we need to convert the delta.
+          let minDelta = expected.delta * minLineHeight;
+          let maxDelta = expected.delta * maxLineHeight;
+          isbetween(e.detail, minDelta, maxDelta, "wrong pixel scroll event delta");
+        }
+      }
+      e.stopPropagation();
+    }
+    // Set up the expected values.
+    if (aKind == 0 || aKind == 1) {
+      expectedEvents.push({
+        type: "DOMMouseScroll",
+        axis: aAxis,
+        delta: aDelta,
+        hasPixels: (aKind == 1),
+        shiftKey: aShiftKey,
+        ctrlKey: aCtrlKey,
+        altKey: aAltKey,
+        metaKey: aMetaKey
+      });
+    }
+    if (aKind == 0 || aKind == 2) {
+      expectedEvents.push({
+        type: "MozMousePixelScroll",
+        axis: aAxis,
+        delta: aDelta,
+        shiftKey: aShiftKey,
+        ctrlKey: aCtrlKey,
+        altKey: aAltKey,
+        metaKey: aMetaKey
+      });
+    }
+    deltaUnit = aKind == 2 ? "pixels" : "lines";
+    
+    document.addEventListener("DOMMouseScroll", listener, true);
+    document.addEventListener("MozMousePixelScroll", listener, true);
+  
+    // Send the event to the documentElement.
+    synthesizeMouseScroll(document.documentElement, 10, 10, expectedEvents[0]);
+  
+    document.removeEventListener("DOMMouseScroll", listener, true);
+    document.removeEventListener("MozMousePixelScroll", listener, true);
+  
+    // expectedEvents should be empty now. If it's not, print errors.
+    expectedEvents.forEach(function(e) {
+      ok(false, "Didn't receive expected event: " + JSON.toString(e));
+    });
+  };
+  let i = 0;
+  [0, 1, 2].forEach(function(aKind) {
+    ["horizontal", "vertical"].forEach(function(aAxis) {
+      [false, true].forEach(function(aShift) {
+        [false, true].forEach(function(aCtrl) {
+          [false, true].forEach(function(aAlt) {
+            [false, true].forEach(function(aMeta) {
+              helper(aAxis, [-5, -1, 0, 1, 5][i++ % 5], aKind, aShift, aCtrl, aAlt, aMeta);
+            });
+          });
+        });
+      });
+    });
+  });
+}
+
+function testDefaultHandling() {
+  let scrollbox = document.getElementById("scrollbox");
+  let currentTest = "";
+  
+  function scrollWithPreventDefault(aEvent, aDoConsume) {
+    function listener(e) {
+      if (aDoConsume[e.type])
+        e.preventDefault();
+    }
+    scrollbox.addEventListener("DOMMouseScroll", listener, true);
+    scrollbox.addEventListener("MozMousePixelScroll", listener, true);
+    synthesizeMouseScroll(scrollbox, 10, 10, aEvent);
+    scrollbox.removeEventListener("DOMMouseScroll", listener, true);
+    scrollbox.removeEventListener("MozMousePixelScroll", listener, true);
+  }
+  
+  function helper(aType, aHasPixels, aAxis, aStart, aDelta, aConsumeLine, aConsumePixel, aPositionShouldChange) {
+    scrollbox.scrollLeft = aStart;
+    scrollbox.scrollTop = aStart;
+    scrollWithPreventDefault({
+      type: aType,
+      axis: aAxis,
+      hasPixels: aHasPixels,
+      delta: aDelta
+    }, {
+      "DOMMouseScroll": aConsumeLine,
+      "MozMousePixelScroll": aConsumePixel
+    });
+    let newPos = scrollbox[aAxis == "horizontal" ? "scrollLeft" : "scrollTop"];
+    let newPosWrongAxis = scrollbox[aAxis == "horizontal" ? "scrollTop" : "scrollLeft"];
+
+    is(newPosWrongAxis, aStart, currentTest + " wrong axis scrolled - type: " + aType);
+
+    if (aPositionShouldChange) {
+      if (aType == "MozMousePixelScroll") {
+        // aDelta is in pixels, no conversion necessary
+        is(newPos, aStart + aDelta, currentTest + " wrong scroll position - type: " + aType);
+      } else {
+        // Use minLineHeight and maxLineHeight as an estimate for the conversion factor.
+        isbetween(newPos, aStart + aDelta * minLineHeight, aStart + aDelta * maxLineHeight,
+                  currentTest + " wrong scroll position - type: " + aType);
+      }
+    } else {
+      is(newPos, aStart, currentTest + " The scroll position shouldn't have changed. - type: " + aType);
+    }
+  }
+
+  ["horizontal", "vertical"].forEach(function(aAxis) {
+    [-5, 5].forEach(function(aDelta) {
+      [false, true].forEach(function(aConsumeLine) {
+        [false, true].forEach(function(aConsumePixel) {
+          let shouldScroll = !aConsumeLine && !aConsumePixel;
+          
+          currentTest = "normal DOMMouseScroll: only scroll if neither line nor pixel scroll are consumed.";
+          helper("DOMMouseScroll", false, aAxis, 4000, aDelta, aConsumeLine, aConsumePixel, shouldScroll);
+          
+          currentTest = "DOMMouseScroll with hasPixels: never scroll.";
+          helper("DOMMouseScroll", true, aAxis, 4000, aDelta, aConsumeLine, aConsumePixel, false);
+          
+          currentTest = "MozMousePixelScroll (consumed: " + aConsumePixel +
+                        ") with preceding DOMMouseScroll (consumed: " + aConsumeLine +
+                        "): " + (shouldScroll ? "scroll." : "don't scroll.");
+          // It shouldn't matter:
+          //  1. whether hasPixels is set on the preceding DOMMouseScroll event or
+          //  2. whether the preceding DOMMouseScroll event's MozMousePixelScroll event is consumed.
+          helper("DOMMouseScroll", true, aAxis, 4000, aDelta, aConsumeLine, false, false);
+          helper("MozMousePixelScroll", false, aAxis, 4000, aDelta, false, aConsumePixel, shouldScroll);
+          helper("DOMMouseScroll", false, aAxis, 4000, aDelta, aConsumeLine, false, !aConsumeLine);
+          helper("MozMousePixelScroll", false, aAxis, 4000, aDelta, false, aConsumePixel, shouldScroll);
+          helper("DOMMouseScroll", false, aAxis, 4000, aDelta, aConsumeLine, true, false);
+          helper("MozMousePixelScroll", false, aAxis, 4000, aDelta, false, aConsumePixel, shouldScroll);
+        });
+      });
+    });
+  });
+}
+
+function runTests() {
+  netscape.security.PrivilegeManager.enablePrivilege("UniversalXPConnect");
+  Components.utils.import("resource://gre/modules/JSON.jsm");
+
+  testEventDispatching();
+  testDefaultHandling();
+
+  SimpleTest.finish();
+}
+
+window.onload = function() { setTimeout(runTests, 0); };
+SimpleTest.waitForExplicitFinish();
+
+]]></script>
+
+</window>
--- a/dom/public/idl/base/nsIDOMWindowUtils.idl
+++ b/dom/public/idl/base/nsIDOMWindowUtils.idl
@@ -125,30 +125,31 @@ interface nsIDOMWindowUtils : nsISupport
                       in long aY,
                       in long aButton,
                       in long aClickCount,
                       in long aModifiers);
 
   /** Synthesize a mouse scroll event for a window. The event types supported
    *  are: 
    *    DOMMouseScroll
+   *    MozMousePixelScroll
    *
    * Events are sent in coordinates offset by aX and aY from the window.
    *
    * Cannot be accessed from unprivileged context (not content-accessible)
    * Will throw a DOM security error if called without UniversalXPConnect
    * privileges.
    *
    * @param aType event type
    * @param aX x offset
    * @param aY y offset
    * @param aButton button to synthesize
    * @param aScrollFlags flag bits --- see nsMouseScrollFlags in nsGUIEvent.h
    * @param aDelta the direction and amount to scroll (in lines or pixels,
-   * depending on whether kIsPixels is set in aScrollFlags)
+   * depending on the event type)
    * @param aModifiers modifiers pressed, using constants defined in nsIDOMNSEvent
    */
   void sendMouseScrollEvent(in AString aType,
                             in long aX,
                             in long aY,
                             in long aButton,
                             in long aScrollFlags,
                             in long aDelta,
--- a/dom/src/base/nsDOMWindowUtils.cpp
+++ b/dom/src/base/nsDOMWindowUtils.cpp
@@ -265,16 +265,18 @@ nsDOMWindowUtils::SendMouseScrollEvent(c
   // get the widget to send the event to
   nsCOMPtr<nsIWidget> widget = GetWidget();
   if (!widget)
     return NS_ERROR_NULL_POINTER;
 
   PRInt32 msg;
   if (aType.EqualsLiteral("DOMMouseScroll"))
     msg = NS_MOUSE_SCROLL;
+  else if (aType.EqualsLiteral("MozMousePixelScroll"))
+    msg = NS_MOUSE_PIXEL_SCROLL;
   else
     return NS_ERROR_UNEXPECTED;
 
   nsMouseScrollEvent event(PR_TRUE, msg, widget);
   event.isShift = (aModifiers & nsIDOMNSEvent::SHIFT_MASK) ? PR_TRUE : PR_FALSE;
   event.isControl = (aModifiers & nsIDOMNSEvent::CONTROL_MASK) ? PR_TRUE : PR_FALSE;
   event.isAlt = (aModifiers & nsIDOMNSEvent::ALT_MASK) ? PR_TRUE : PR_FALSE;
   event.isMeta = (aModifiers & nsIDOMNSEvent::META_MASK) ? PR_TRUE : PR_FALSE;
--- a/modules/libpref/src/init/all.js
+++ b/modules/libpref/src/init/all.js
@@ -897,16 +897,19 @@ pref("middlemouse.scrollbarPosition", fa
 // Clipboard behavior
 pref("clipboard.autocopy", false);
 
 // mouse wheel scroll transaction period of time (in milliseconds)
 pref("mousewheel.transaction.timeout", 1500);
 // mouse wheel scroll transaction is held even if the mouse cursor is moved.
 pref("mousewheel.transaction.ignoremovedelay", 100);
 
+// Macbook touchpad two finger pixel scrolling
+pref("mousewheel.enable_pixel_scrolling", true);
+
 // 0=lines, 1=pages, 2=history , 3=text size
 pref("mousewheel.withnokey.action",0);
 pref("mousewheel.withnokey.numlines",1);	
 pref("mousewheel.withnokey.sysnumlines",true);
 pref("mousewheel.withcontrolkey.action",0);
 pref("mousewheel.withcontrolkey.numlines",1);
 pref("mousewheel.withcontrolkey.sysnumlines",true);
 // mousewheel.withshiftkey, see the Mac note below.
--- a/testing/mochitest/tests/SimpleTest/EventUtils.js
+++ b/testing/mochitest/tests/SimpleTest/EventUtils.js
@@ -225,57 +225,57 @@ function synthesizeMouse(aTarget, aOffse
 }
 
 /**
  * Synthesize a mouse scroll event on a target. The actual client point is determined
  * by taking the aTarget's client box and offseting it by aOffsetX and
  * aOffsetY.
  *
  * aEvent is an object which may contain the properties:
- *   shiftKey, ctrlKey, altKey, metaKey, accessKey, button, type, axis, units, delta
+ *   shiftKey, ctrlKey, altKey, metaKey, accessKey, button, type, axis, delta, hasPixels
  *
- * If the type is specified, an mouse scroll event of that type is fired. Otherwise,
+ * If the type is specified, a mouse scroll event of that type is fired. Otherwise,
  * "DOMMouseScroll" is used.
  *
  * If the axis is specified, it must be one of "horizontal" or "vertical". If not specified,
  * "vertical" is used.
  * 
  * 'delta' is the amount to scroll by (can be positive or negative). It must
- * be specified. 'units' is the units of 'delta', either "pixels" or "lines"; "lines"
- * is the default if 'units' is ommitted.
+ * be specified.
+ *
+ * 'hasPixels' specifies whether kHasPixels should be set in the scrollFlags.
  *
  * aWindow is optional, and defaults to the current window object.
  */
 function synthesizeMouseScroll(aTarget, aOffsetX, aOffsetY, aEvent, aWindow)
 {
   netscape.security.PrivilegeManager.enablePrivilege('UniversalXPConnect');
 
   if (!aWindow)
     aWindow = window;
 
   var utils = aWindow.QueryInterface(Components.interfaces.nsIInterfaceRequestor).
                       getInterface(Components.interfaces.nsIDOMWindowUtils);
   if (utils) {
     // See nsMouseScrollFlags in nsGUIEvent.h
     const kIsVertical = 0x02;
     const kIsHorizontal = 0x04;
-    const kIsPixels = 0x08;
+    const kHasPixels = 0x08;
 
     var button = aEvent.button || 0;
     var modifiers = _parseModifiers(aEvent);
 
     var left = aTarget.boxObject.x;
     var top = aTarget.boxObject.y;
 
     var type = aEvent.type || "DOMMouseScroll";
     var axis = aEvent.axis || "vertical";
-    var units = aEvent.units || "lines";
     var scrollFlags = (axis == "horizontal") ? kIsHorizontal : kIsVertical;
-    if (units == "pixels") {
-      scrollFlags |= kIsPixels;
+    if (aEvent.hasPixels) {
+      scrollFlags |= kHasPixels;
     }
     utils.sendMouseScrollEvent(type, left + aOffsetX, top + aOffsetY, button,
                                scrollFlags, aEvent.delta, modifiers);
   }
 }
 
 /**
  * Synthesize a key event. It is targeted at whatever would be targeted by an
--- a/toolkit/content/tests/widgets/test_mousescroll.xul
+++ b/toolkit/content/tests/widgets/test_mousescroll.xul
@@ -68,113 +68,146 @@ https://bugzilla.mozilla.org/show_bug.cg
       <vbox style="width:100px; height:40px; background:white;"/>
       <vbox style="width:100px; height:40px; background:black;"/>
   </arrowscrollbox>
   
   <!-- test code goes here -->
   <script type="application/javascript"><![CDATA[
 
 /** Test for Bug 378028 **/
+/*   and for Bug 350471 **/
 SimpleTest.waitForExplicitFinish();
 
+/* There are three kinds of scroll events:
+    1. line scrolls without hasPixels
+    2. line scrolls with hasPixels
+    3. pixel scrolls
+   Listboxes and arrowscrollboxes (DOM event scrolling) should only react to
+   line scrolls and ignore hasPixels.
+   Richlistboxes ("native" scrolling) should be scrollable by kind 1 and 3.
+*/
+const kinds = [
+  { eventType: "DOMMouseScroll", hasPixels: false, shouldScrollDOM: true, shouldScrollNative: true },
+  { eventType: "DOMMouseScroll", hasPixels: true, shouldScrollDOM: true, shouldScrollNative: false },
+  { eventType: "MozMousePixelScroll", hasPixels: false, shouldScrollDOM: false, shouldScrollNative: true }
+];
+
+
 function testListbox(id)
 {
   var listbox = document.getElementById(id);
   
-  function helper(aStart, aDelta)
+  function helper(aStart, aDelta, aKind)
   {
     listbox.scrollToIndex(aStart);
     synthesizeMouseScroll(listbox, 10, 10,
-                          {axis:"vertical", delta:aDelta});
-    is(listbox.getIndexOfFirstVisibleRow(), aStart + aDelta,
-       "mouse-scroll of '" + id + "' vertical starting " + aStart + " delta " + aDelta);
+                          {axis:"vertical", delta:aDelta, type:aKind.eventType,
+                           hasPixels:aKind.hasPixels});
+    is(listbox.getIndexOfFirstVisibleRow(), aKind.shouldScrollDOM ? aStart + aDelta : aStart,
+       "mouse-scroll of '" + id + "' vertical starting " + aStart + " delta " + aDelta
+       + " eventType " + aKind.eventType + " hasPixels " + aKind.hasPixels);
 
     // Check that horizontal scrolling has no effect
     listbox.scrollToIndex(aStart);
     synthesizeMouseScroll(listbox, 10, 10,
-                          {axis:"horizontal", delta:aDelta});  
+                          {axis:"horizontal", delta:aDelta, type:aKind.eventType,
+                           hasPixels:aKind.hasPixels});  
     is(listbox.getIndexOfFirstVisibleRow(), aStart,
-       "mouse-scroll of '" + id + "' horizontal starting " + aStart + " delta " + aDelta);
+       "mouse-scroll of '" + id + "' horizontal starting " + aStart + " delta " + aDelta
+       + " eventType " + aKind.eventType + " hasPixels " + aKind.hasPixels);
   }
-  
-  helper(2, -1);
-  helper(2, 1);
-  helper(2, -2);
-  helper(2, 2);
+  kinds.forEach(function(aKind) {
+    helper(2, -1, aKind);
+    helper(2, 1, aKind);
+    helper(2, -2, aKind);
+    helper(2, 2, aKind);
+  });
 }
 
 function testRichListbox(id)
 {
   var listbox = document.getElementById(id);
   
-  function helper(aStart, aDelta, aExpected)
+  function helper(aStart, aDelta, aExpected, aKind)
   {
     listbox.scrollToIndex(aStart);
     synthesizeMouseScroll(listbox, 10, 10,
-                          {axis:"vertical", delta:aDelta});
-    is(listbox.getIndexOfFirstVisibleRow(), aExpected,
-       "mouse-scroll of '" + id + "' vertical starting " + aStart + " delta " + aDelta);
+                          {axis:"vertical", delta:aDelta, type:aKind.eventType,
+                           hasPixels:aKind.hasPixels});
+    is(listbox.getIndexOfFirstVisibleRow(), aKind.shouldScrollNative ? aExpected : aStart,
+       "mouse-scroll of '" + id + "' vertical starting " + aStart + " delta " + aDelta
+       + " eventType " + aKind.eventType + " hasPixels " + aKind.hasPixels);
 
     // Check that horizontal scrolling has no effect
     listbox.scrollToIndex(aStart);
     synthesizeMouseScroll(listbox, 10, 10,
-                          {axis:"horizontal", delta:aDelta});  
+                          {axis:"horizontal", delta:aDelta, type:aKind.eventType,
+                           hasPixels:aKind.hasPixels});  
     is(listbox.getIndexOfFirstVisibleRow(), aStart,
-       "mouse-scroll of '" + id + "' horizontal starting " + aStart + " delta " + aDelta);
+       "mouse-scroll of '" + id + "' horizontal starting " + aStart + " delta " + aDelta
+       + " eventType " + aKind.eventType + " hasPixels " + aKind.hasPixels);
   }
   
   // richlistbox currently uses native XUL scrolling, so the "line"
   // amounts don't necessarily correspond 1-to-1 with listbox items. So
   // we just check that scrolling up/down a lot hits the first/last items
-  helper(2, -100, 0);
-  helper(2, 100, listbox.getRowCount() - listbox.getNumberOfVisibleRows());
+  kinds.forEach(function(aKind) {
+    helper(2, -100, 0, aKind);
+    helper(2, 100, listbox.getRowCount() - listbox.getNumberOfVisibleRows(), aKind);
+  });
 }
 
 function testArrowScrollbox(id)
 {
   var scrollbox = document.getElementById(id);
   var scrollBoxObject = scrollbox.scrollBoxObject;
   var orient = scrollbox.getAttribute("orient");
   
-  function helper(aStart, aDelta, aExpected)
+  function helper(aStart, aDelta, aExpected, aKind)
   {
     var xpos = {};
     var ypos = {};
     var pos = orient == "horizontal" ? xpos : ypos;
 
     scrollBoxObject.scrollTo(aStart, aStart);
     synthesizeMouseScroll(scrollbox, 5, 5,
-                          {axis:"vertical", delta:aDelta});
+                          {axis:"vertical", delta:aDelta, type:aKind.eventType,
+                           hasPixels:aKind.hasPixels});
     scrollBoxObject.getPosition(xpos, ypos);
     // Note, vertical mouse scrolling is allowed to scroll horizontal
     // arrowscrollboxes, because many users have no horizontal mouse scroll
     // capability
-    is(pos.value, aExpected,
-       "mouse-scroll of '" + id + "' vertical starting " + aStart + " delta " + aDelta);
+    is(pos.value, aKind.shouldScrollDOM ? aExpected : aStart,
+       "mouse-scroll of '" + id + "' vertical starting " + aStart + " delta " + aDelta
+       + " eventType " + aKind.eventType + " hasPixels " + aKind.hasPixels);
 
     scrollBoxObject.scrollTo(aStart, aStart);
     synthesizeMouseScroll(scrollbox, 5, 5,
-                          {axis:"horizontal", delta:aDelta});
+                          {axis:"horizontal", delta:aDelta, type:aKind.eventType,
+                           hasPixels:aKind.hasPixels});
     // horizontal mouse scrolling is never allowed to scroll vertical
     // arrowscrollboxes
     scrollBoxObject.getPosition(xpos, ypos);
-    var expected = orient == "horizontal" ? aExpected : aStart;
+    var expected = (aKind.shouldScrollDOM && (orient == "horizontal")) ? aExpected : aStart;
     is(pos.value, expected,
-       "mouse-scroll of '" + id + "' horizontal starting " + aStart + " delta " + aDelta);
+       "mouse-scroll of '" + id + "' horizontal starting " + aStart + " delta " + aDelta
+       + " eventType " + aKind.eventType + " hasPixels " + aKind.hasPixels);
   }
 
   var scrolledWidth = {};
   var scrolledHeight = {};
   scrollBoxObject.getScrolledSize(scrolledWidth, scrolledHeight);
   var scrollMaxX = scrolledWidth.value - scrollBoxObject.width;
   var scrollMaxY = scrolledHeight.value - scrollBoxObject.height;
   var scrollMax = orient == "horizontal" ? scrollMaxX : scrollMaxY;
 
-  helper(50, -100, 0);
-  helper(50, 100, scrollMax);
+  kinds.forEach(function(aKind) {
+    helper(50, -100, 0, aKind);
+    helper(50, 100, scrollMax, aKind);
+  });
 }
 
 function runTests()
 {
   testRichListbox("richlistbox");
   testListbox("listbox");
   testArrowScrollbox("hscrollbox");
   testArrowScrollbox("vscrollbox");
--- a/toolkit/content/tests/widgets/tree_shared.js
+++ b/toolkit/content/tests/widgets/tree_shared.js
@@ -1091,34 +1091,47 @@ function testtag_tree_column_reorder()
   is(document.treecolDragging, null, "drag to itself completed");
 
   // XXX roc should this be here???
   SimpleTest.finish();
 }
 
 function testtag_tree_mousescroll(aTree)
 {
-  function helper(aStart, aDelta)
+  /* Scroll event kinds, see test_mousescroll.xul */
+  const kinds = [
+    { eventType: "DOMMouseScroll", hasPixels: false, shouldScrollDOM: true, shouldScrollNative: true },
+    { eventType: "DOMMouseScroll", hasPixels: true, shouldScrollDOM: true, shouldScrollNative: false },
+    { eventType: "MozMousePixelScroll", hasPixels: false, shouldScrollDOM: false, shouldScrollNative: true }
+  ];
+  function helper(aStart, aDelta, aKind)
   {
     aTree.treeBoxObject.scrollToRow(aStart);
     synthesizeMouseScroll(aTree.body, 1, 1,
-                         {type:"DOMMouseScroll", axis:"vertical", delta:aDelta});
-    is(aTree.treeBoxObject.getFirstVisibleRow(), aStart + aDelta, "mouse-scroll vertical starting " + aStart + " delta " + aDelta);
+                          {axis:"vertical", delta:aDelta, type:aKind.eventType,
+                           hasPixels:aKind.hasPixels});
+    var expected = aKind.shouldScrollDOM ? aStart + aDelta : aStart;
+    is(aTree.treeBoxObject.getFirstVisibleRow(), expected, "mouse-scroll vertical starting " + aStart + " delta " + aDelta
+       + " eventType " + aKind.eventType + " hasPixels " + aKind.hasPixels);
 
     aTree.treeBoxObject.scrollToRow(aStart);
     // Check that horizontal scrolling has no effect
     synthesizeMouseScroll(aTree.body, 1, 1,
-                          {type:"DOMMouseScroll", axis:"horizontal", delta:aDelta});  
-    is(aTree.treeBoxObject.getFirstVisibleRow(), aStart, "mouse-scroll horizontal starting " + aStart + " delta " + aDelta);
+                          {axis:"horizontal", delta:aDelta, type:aKind.eventType,
+                           hasPixels:aKind.hasPixels});  
+    is(aTree.treeBoxObject.getFirstVisibleRow(), aStart, "mouse-scroll horizontal starting " + aStart + " delta " + aDelta
+       + " eventType " + aKind.eventType + " hasPixels " + aKind.hasPixels);
   }
   
-  helper(2, -1);
-  helper(2, 1);
-  helper(2, -2);
-  helper(2, 2);
+  kinds.forEach(function(aKind) {
+    helper(2, -1, aKind);
+    helper(2, 1, aKind);
+    helper(2, -2, aKind);
+    helper(2, 2, aKind);
+  });
 }
 
 function synthesizeColumnDrag(aTree, aMouseDownColumnNumber, aMouseUpColumnNumber, aAfter)
 {
   var columns = getSortedColumnArray(aTree);
 
   var down = columns[aMouseDownColumnNumber].element;
   var up   = columns[aMouseUpColumnNumber].element;
--- a/widget/public/nsGUIEvent.h
+++ b/widget/public/nsGUIEvent.h
@@ -266,16 +266,17 @@ class nsHashKey;
 // NS_XUL_COMMAND used to be here     (NS_XUL_EVENT_START+4)
 #define NS_XUL_BROADCAST              (NS_XUL_EVENT_START+5)
 #define NS_XUL_COMMAND_UPDATE         (NS_XUL_EVENT_START+6)
 //@}
 
 // Scroll events
 #define NS_MOUSE_SCROLL_START         1600
 #define NS_MOUSE_SCROLL               (NS_MOUSE_SCROLL_START)
+#define NS_MOUSE_PIXEL_SCROLL         (NS_MOUSE_SCROLL_START + 1)
 
 #define NS_SCROLLPORT_START           1700
 #define NS_SCROLLPORT_UNDERFLOW       (NS_SCROLLPORT_START)
 #define NS_SCROLLPORT_OVERFLOW        (NS_SCROLLPORT_START+1)
 #define NS_SCROLLPORT_OVERFLOWCHANGED (NS_SCROLLPORT_START+2)
 
 // Mutation events defined elsewhere starting at 1800
 
@@ -844,24 +845,69 @@ public:
   nsCompositionEvent(PRBool isTrusted, PRUint32 msg, nsIWidget *w)
     : nsInputEvent(isTrusted, msg, w, NS_COMPOSITION_EVENT)
   {
   }
 
   nsTextEventReply theReply;
 };
 
+/* Mouse Scroll Events: Line Scrolling, Pixel Scrolling and Common Event Flows
+ *
+ * There are two common event flows:
+ *  (1) Normal line scrolling:
+ *      1. An NS_MOUSE_SCROLL event without kHasPixels is dispatched to Gecko.
+ *      2. A DOMMouseScroll event is sent into the DOM.
+ *      3. A MozMousePixelScroll event is sent into the DOM.
+ *      4. If neither event has been consumed, the default handling of the
+ *         NS_MOUSE_SCROLL event is executed.
+ *
+ *  (2) Pixel scrolling:
+ *      1. An NS_MOUSE_SCROLL event with kHasPixels is dispatched to Gecko.
+ *      2. A DOMMouseScroll event is sent into the DOM.
+ *      3. No scrolling takes place in the default handler.
+ *      4. An NS_MOUSE_PIXEL_SCROLL event is dispatched to Gecko.
+ *      5. A MozMousePixelScroll event is sent into the DOM.
+ *      6. If neither the NS_MOUSE_PIXELSCROLL event nor the preceding
+ *         NS_MOUSE_SCROLL event have been consumed, the default handler scrolls.
+ *      7. Steps 4.-6. are repeated for every pixel scroll that belongs to
+ *         the announced line scroll. Once enough pixels have been sent to
+ *         complete a line, a new NS_MOUSE_SCROLL event is sent (goto step 1.).
+ *
+ * If a DOMMouseScroll event has been preventDefaulted, the associated
+ * following MozMousePixelScroll events are still sent - they just don't result
+ * in any scrolling (their default handler isn't executed).
+ *
+ * How many pixel scrolls make up one line scroll is decided in the widget layer
+ * where the NS_MOUSE(_PIXEL)_SCROLL events are created.
+ *
+ * This event flow model satisfies several requirements:
+ *  - DOMMouseScroll handlers don't need to be aware of the existence of pixel
+ *    scrolling.
+ *  - preventDefault on a DOMMouseScroll event results in no scrolling.
+ *  - DOMMouseScroll events aren't polluted with a kHasPixels flag.
+ *  - You can make use of pixel scroll DOM events (MozMousePixelScroll).
+ */
+
 class nsMouseScrollEvent : public nsMouseEvent_base
 {
 public:
   enum nsMouseScrollFlags {
     kIsFullPage =   1 << 0,
     kIsVertical =   1 << 1,
     kIsHorizontal = 1 << 2,
-    kIsPixels =     1 << 3
+    kHasPixels =    1 << 3  // Marks line scroll events that are provided as
+                            // a fallback for pixel scroll events.
+                            // These scroll events are used by things that can't
+                            // be scrolled pixel-wise, like trees. You should
+                            // ignore them when processing pixel scroll events
+                            // to avoid double-processing the same scroll gesture.
+                            // When kHasPixels is set, the event is guaranteed to
+                            // be followed up by an event that contains pixel
+                            // scrolling information.
   };
 
   nsMouseScrollEvent(PRBool isTrusted, PRUint32 msg, nsIWidget *w)
     : nsMouseEvent_base(isTrusted, msg, w, NS_MOUSE_SCROLL_EVENT),
       scrollFlags(0), delta(0)
   {
   }
 
--- a/widget/src/cocoa/nsChildView.h
+++ b/widget/src/cocoa/nsChildView.h
@@ -94,16 +94,32 @@ extern "C" long TSMProcessRawKeyEvent(Ev
 
 // Return Cocoa event's corresponding Carbon event.  Not initialized (on
 // synthetic events) until the OS actually "sends" the event.  This method
 // has been present in the same form since at least OS X 10.2.8.
 - (EventRef)_eventRef;
 
 @end
 
+// Needed to support pixel scrolling.
+// See http://developer.apple.com/qa/qa2005/qa1453.html.
+// kEventMouseScroll is only defined on 10.5+. Using the moz prefix avoids
+// potential symbol conflicts.
+// This should be changed when 10.4 support is dropped.
+enum {
+  mozkEventMouseScroll             = 11
+};
+
+// Support for pixel scroll deltas, not part of NSEvent.h
+// See http://lists.apple.com/archives/cocoa-dev/2007/Feb/msg00050.html
+@interface NSEvent (DeviceDelta)
+  - (float)deviceDeltaX;
+  - (float)deviceDeltaY;
+@end
+
 @interface ChildView : NSView<
 #ifdef ACCESSIBILITY
                               mozAccessible,
 #endif
                               mozView, NSTextInput>
 {
 @private
   NSWindow* mWindow; // shortcut to the top window, [WEAK]
--- a/widget/src/cocoa/nsChildView.mm
+++ b/widget/src/cocoa/nsChildView.mm
@@ -3488,89 +3488,131 @@ static nsEventStatus SendGeckoMouseEnter
 // Handle an NSScrollWheel event for a single axis only.
 -(void)scrollWheel:(NSEvent*)theEvent forAxis:(enum nsMouseScrollEvent::nsMouseScrollFlags)inAxis
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;
 
   if (!mGeckoChild)
     return;
 
-  float scrollDelta;
-
-  if (inAxis & nsMouseScrollEvent::kIsVertical)
-    scrollDelta = -[theEvent deltaY];
-  else if (inAxis & nsMouseScrollEvent::kIsHorizontal)
-    scrollDelta = -[theEvent deltaX];
-  else
+  float scrollDelta = 0;
+  float scrollDeltaPixels = 0;
+  PRBool checkPixels = PR_TRUE;
+
+  nsCOMPtr<nsIPrefBranch> prefs = do_GetService(NS_PREFSERVICE_CONTRACTID);
+  if (prefs)
+    prefs->GetBoolPref("mousewheel.enable_pixel_scrolling", &checkPixels);
+
+  EventRef theCarbonEvent = [theEvent _eventRef];
+  UInt32 carbonEventKind = theCarbonEvent ? ::GetEventKind(theCarbonEvent) : 0;
+  // Calling deviceDeltaX or deviceDeltaY on theEvent will trigger a Cocoa
+  // assertion and an Objective-C NSInternalInconsistencyException if the
+  // underlying "Carbon" event doesn't contain pixel scrolling information.
+  // For these events, carbonEventKind is kEventMouseWheelMoved instead of
+  // kEventMouseScroll.
+  if (carbonEventKind != mozkEventMouseScroll)
+    checkPixels = PR_FALSE;
+  // Some scrolling devices supports pixel scrolling, e.g. a Macbook
+  // touchpad or a Mighty Mouse. On those devices, [event deviceDeltaX/Y]
+  // contains the amount of pixels to scroll. 
+  if (inAxis & nsMouseScrollEvent::kIsVertical) {
+    scrollDelta       = -[theEvent deltaY];
+    if (checkPixels && (scrollDelta == 0 || scrollDelta != floor(scrollDelta))) {
+      scrollDeltaPixels = -[theEvent deviceDeltaY];
+    }
+  } else if (inAxis & nsMouseScrollEvent::kIsHorizontal) {
+    scrollDelta       = -[theEvent deltaX];
+    if (checkPixels && (scrollDelta == 0 || scrollDelta != floor(scrollDelta))) {
+      scrollDeltaPixels = -[theEvent deviceDeltaX];
+    }
+  } else {
     return; // caller screwed up
-
-  if (scrollDelta == 0)
-    // No sense in firing off a Gecko event.  Note that as of 10.4 Tiger,
-    // a single NSScrollWheel event might result in deltaX = deltaY = 0.
-    return;
-  
-  nsMouseScrollEvent geckoEvent(PR_TRUE, NS_MOUSE_SCROLL, nsnull);
-  [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
-  geckoEvent.scrollFlags |= inAxis;
-
-  // Gecko only understands how to scroll by an integer value.  Using floor
-  // and ceil is better than truncating the fraction, especially when
-  // |delta| < 1.
-  if (scrollDelta < 0)
-    geckoEvent.delta = (PRInt32)floorf(scrollDelta);
-  else
-    geckoEvent.delta = (PRInt32)ceilf(scrollDelta);
-
-  nsAutoRetainCocoaObject kungFuDeathGrip(self);
-  mGeckoChild->DispatchWindowEvent(geckoEvent);
-  if (!mGeckoChild)
-    return;
-
-  // dispatch scroll wheel carbon event for plugins
-  {
-    EventRef theEvent;
-    OSStatus err = ::MacCreateEvent(NULL,
-                          kEventClassMouse,
-                          kEventMouseWheelMoved,
-                          TicksToEventTime(TickCount()),
-                          kEventAttributeUserEvent,
-                          &theEvent);
-    if (err == noErr) {
-      EventMouseWheelAxis axis;
-      if (inAxis & nsMouseScrollEvent::kIsVertical)
-        axis = kEventMouseWheelAxisY;
-      else if (inAxis & nsMouseScrollEvent::kIsHorizontal)
-        axis = kEventMouseWheelAxisX;
-      
-      SetEventParameter(theEvent,
-                            kEventParamMouseWheelAxis,
-                            typeMouseWheelAxis,
-                            sizeof(EventMouseWheelAxis),
-                            &axis);
-
-      SInt32 delta = (SInt32)-geckoEvent.delta;
-      SetEventParameter(theEvent,
-                            kEventParamMouseWheelDelta,
-                            typeLongInteger,
-                            sizeof(SInt32),
-                            &delta);
-
-      Point mouseLoc;
-      ::GetGlobalMouse(&mouseLoc);
-      SetEventParameter(theEvent,
-                            kEventParamMouseLocation,
-                            typeQDPoint,
-                            sizeof(Point),
-                            &mouseLoc);
-      
-      ::SendEventToEventTarget(theEvent, GetWindowEventTarget((WindowRef)[[self window] windowRef]));
-      ReleaseEvent(theEvent);
+  }
+
+  BOOL hasPixels = (scrollDeltaPixels != 0);
+
+  if (!hasPixels && scrollDelta == 0)
+    // No sense in firing off a Gecko event.
+     return;
+
+  if (scrollDelta != 0) {
+    // Send the line scroll event.
+    nsMouseScrollEvent geckoEvent(PR_TRUE, NS_MOUSE_SCROLL, nsnull);
+    [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+    geckoEvent.scrollFlags |= inAxis;
+
+    if (hasPixels)
+      geckoEvent.scrollFlags |= nsMouseScrollEvent::kHasPixels;
+
+    // Gecko only understands how to scroll by an integer value. Using floor
+    // and ceil is better than truncating the fraction, especially when
+    // |delta| < 1.
+    if (scrollDelta < 0)
+      geckoEvent.delta = (PRInt32)floorf(scrollDelta);
+    else
+      geckoEvent.delta = (PRInt32)ceilf(scrollDelta);
+
+    nsAutoRetainCocoaObject kungFuDeathGrip(self);
+    mGeckoChild->DispatchWindowEvent(geckoEvent);
+    if (!mGeckoChild)
+      return;
+
+    // dispatch scroll wheel carbon event for plugins
+    {
+      EventRef theEvent;
+      OSStatus err = ::MacCreateEvent(NULL,
+                                      kEventClassMouse,
+                                      kEventMouseWheelMoved,
+                                      TicksToEventTime(TickCount()),
+                                      kEventAttributeUserEvent,
+                                      &theEvent);
+      if (err == noErr) {
+        EventMouseWheelAxis axis;
+        if (inAxis & nsMouseScrollEvent::kIsVertical)
+          axis = kEventMouseWheelAxisY;
+        else if (inAxis & nsMouseScrollEvent::kIsHorizontal)
+          axis = kEventMouseWheelAxisX;
+        
+        SetEventParameter(theEvent,
+                          kEventParamMouseWheelAxis,
+                          typeMouseWheelAxis,
+                          sizeof(EventMouseWheelAxis),
+                          &axis);
+        
+        SInt32 delta = (SInt32)-geckoEvent.delta;
+        SetEventParameter(theEvent,
+                          kEventParamMouseWheelDelta,
+                          typeLongInteger,
+                          sizeof(SInt32),
+                          &delta);
+        
+        Point mouseLoc;
+        ::GetGlobalMouse(&mouseLoc);
+        SetEventParameter(theEvent,
+                          kEventParamMouseLocation,
+                          typeQDPoint,
+                          sizeof(Point),
+                          &mouseLoc);
+        
+        ::SendEventToEventTarget(theEvent, GetWindowEventTarget((WindowRef)[[self window] windowRef]));
+        ReleaseEvent(theEvent);
+      }
     }
   }
 
+  if (hasPixels) {
+    // Send the pixel scroll event.
+    nsMouseScrollEvent geckoEvent(PR_TRUE, NS_MOUSE_PIXEL_SCROLL, nsnull);
+    [self convertCocoaMouseEvent:theEvent toGeckoEvent:&geckoEvent];
+    geckoEvent.scrollFlags |= inAxis;
+    geckoEvent.delta = NSToIntRound(scrollDeltaPixels);
+    nsAutoRetainCocoaObject kungFuDeathGrip(self);
+    mGeckoChild->DispatchWindowEvent(geckoEvent);
+  }
+
   NS_OBJC_END_TRY_ABORT_BLOCK;
 }
 
 
 -(void)scrollWheel:(NSEvent*)theEvent
 {
   NS_OBJC_BEGIN_TRY_ABORT_BLOCK;