# HG changeset patch # Parent f8c174b95c40542474eb7613a8670199c084151b # User Chris Lord Bug 703573 - Make document sub-frames scrollable. r= The new Java compositor only handles scrolling the top-level scroll-frame. Use browser.js to detect when a sub-frame is being scrolled and send an 'override' event to Java to tell it to pass us panning events instead of calling the scroll function on the LayerController. diff --git a/mobile/android/base/ui/PanZoomController.java b/mobile/android/base/ui/PanZoomController.java --- a/mobile/android/base/ui/PanZoomController.java +++ b/mobile/android/base/ui/PanZoomController.java @@ -33,16 +33,17 @@ * the provisions above, a recipient may use your version of this file under * the terms of any one of the MPL, the GPL or the LGPL. * * ***** END LICENSE BLOCK ***** */ package org.mozilla.gecko.ui; import org.json.JSONObject; +import org.json.JSONException; import org.mozilla.gecko.gfx.FloatSize; import org.mozilla.gecko.gfx.LayerController; import org.mozilla.gecko.gfx.PointUtils; import org.mozilla.gecko.gfx.RectUtils; import org.mozilla.gecko.FloatUtils; import org.mozilla.gecko.GeckoApp; import org.mozilla.gecko.GeckoAppShell; import org.mozilla.gecko.GeckoEvent; @@ -112,37 +113,60 @@ public class PanZoomController * similar to TOUCHING but after starting a pan */ PANNING_HOLD_LOCKED, /* like PANNING_HOLD, but axis lock still in effect */ PINCHING, /* nth touch-start, where n > 1. this mode allows pan and zoom */ ANIMATED_ZOOM /* animated zoom to a new rect */ } private PanZoomState mState; + private boolean mOverridePanning; + private boolean mOverridePanningAck; + private boolean mOverridePanningPending; + public PanZoomController(LayerController controller) { mController = controller; mX = new Axis(); mY = new Axis(); mState = PanZoomState.NOTHING; populatePositionAndLength(); GeckoAppShell.registerGeckoEventListener("Browser:ZoomToRect", this); GeckoAppShell.registerGeckoEventListener("Browser:ZoomToPageWidth", this); + GeckoAppShell.registerGeckoEventListener("Panning:Override", this); + GeckoAppShell.registerGeckoEventListener("Panning:CancelOverride", this); + GeckoAppShell.registerGeckoEventListener("Gesture:ScrollAck", this); } protected void finalize() throws Throwable { GeckoAppShell.unregisterGeckoEventListener("Browser:ZoomToRect", this); GeckoAppShell.unregisterGeckoEventListener("Browser:ZoomToPageWidth", this); + GeckoAppShell.unregisterGeckoEventListener("Panning:Override", this); + GeckoAppShell.unregisterGeckoEventListener("Panning:CancelOverride", this); + GeckoAppShell.unregisterGeckoEventListener("Gesture:ScrollAck", this); super.finalize(); } - + public void handleMessage(String event, JSONObject message) { Log.i(LOGTAG, "Got message: " + event); try { - if (event.equals("Browser:ZoomToRect")) { + if ("Panning:Override".equals(event)) { + mOverridePanning = true; + mOverridePanningAck = true; + } else if ("Panning:CancelOverride".equals(event)) { + mOverridePanning = false; + } else if ("Gesture:ScrollAck".equals(event)) { + mController.post(new Runnable() { + public void run() { + mOverridePanningAck = true; + if (mOverridePanning && mOverridePanningPending) + updatePosition(); + } + }); + } else if (event.equals("Browser:ZoomToRect")) { if (mController != null) { float scale = mController.getZoomFactor(); float x = (float)message.getDouble("x"); float y = (float)message.getDouble("y"); final RectF zoomRect = new RectF(x, y, x + (float)message.getDouble("w"), y + (float)message.getDouble("h")); mController.post(new Runnable() { @@ -211,16 +235,17 @@ public class PanZoomController private boolean onTouchStart(MotionEvent event) { // user is taking control of movement, so stop // any auto-movement we have going if (mFlingTimer != null) { mFlingTimer.cancel(); mFlingTimer = null; } + mOverridePanning = false; switch (mState) { case ANIMATED_ZOOM: return false; case FLING: case NOTHING: mState = PanZoomState.TOUCHING; mX.velocity = mY.velocity = 0.0f; @@ -412,25 +437,33 @@ public class PanZoomController // should never happen, but handle anyway for robustness Log.e(LOGTAG, "Impossible case " + mState + " when stopped in track"); mState = PanZoomState.PANNING_HOLD_LOCKED; } } mX.setFlingState(Axis.FlingStates.PANNING); mY.setFlingState(Axis.FlingStates.PANNING); - mX.applyEdgeResistance(); mX.displace(); - mY.applyEdgeResistance(); mY.displace(); + + if (!mOverridePanning) { + mX.applyEdgeResistance(); + mY.applyEdgeResistance(); + } + mX.displace(); + mY.displace(); + updatePosition(); } private void fling() { if (mState != PanZoomState.FLING) mX.velocity = mY.velocity = 0.0f; + mX.disableSnap = mY.disableSnap = mOverridePanning; + mX.displace(); mY.displace(); updatePosition(); if (mFlingTimer != null) mFlingTimer.cancel(); boolean stopped = stopped(); mX.startFling(stopped); mY.startFling(stopped); @@ -443,17 +476,42 @@ public class PanZoomController private boolean stopped() { float absVelocity = (float)Math.sqrt(mX.velocity * mX.velocity + mY.velocity * mY.velocity); return absVelocity < STOPPED_THRESHOLD; } private void updatePosition() { - mController.scrollTo(new PointF(mX.viewportPos, mY.viewportPos)); + if (mOverridePanning) { + if (!mOverridePanningAck) { + mOverridePanningPending = true; + return; + } + + mOverridePanningPending = false; + JSONObject json = new JSONObject(); + + try { + json.put("x", mX.displacement); + json.put("y", mY.displacement); + } catch (JSONException e) { + Log.e(LOGTAG, "Error forming Gesture:Scroll message: " + e); + } + + GeckoEvent e = new GeckoEvent("Gesture:Scroll", json.toString()); + GeckoAppShell.sendEventToGecko(e); + mOverridePanningAck = false; + } else { + mController.scrollBy(new PointF(mX.displacement, mY.displacement)); + mX.viewportPos += mX.displacement; + mY.viewportPos += mY.displacement; + } + + mX.displacement = mY.displacement = 0; } // Populates the viewport info and length in the axes. private void populatePositionAndLength() { FloatSize pageSize = mController.getPageSize(); RectF visibleRect = mController.getViewport(); mX.setPageLength(pageSize.width); @@ -463,34 +521,35 @@ public class PanZoomController mY.setPageLength(pageSize.height); mY.viewportPos = visibleRect.top; mY.setViewportLength(visibleRect.height()); } // The callback that performs the fling animation. private class FlingRunnable implements Runnable { public void run() { - populatePositionAndLength(); mX.advanceFling(); mY.advanceFling(); - // If both X and Y axes are overscrolled, we have to wait until both axes have stopped - // to snap back to avoid a jarring effect. - boolean waitingToSnapX = mX.getFlingState() == Axis.FlingStates.WAITING_TO_SNAP; - boolean waitingToSnapY = mY.getFlingState() == Axis.FlingStates.WAITING_TO_SNAP; - if ((mX.getOverscroll() == Axis.Overscroll.PLUS || mX.getOverscroll() == Axis.Overscroll.MINUS) && - (mY.getOverscroll() == Axis.Overscroll.PLUS || mY.getOverscroll() == Axis.Overscroll.MINUS)) - { - if (waitingToSnapX && waitingToSnapY) { - mX.startSnap(); mY.startSnap(); + if (!mOverridePanning) { + // If both X and Y axes are overscrolled, we have to wait until both axes have stopped + // to snap back to avoid a jarring effect. + boolean waitingToSnapX = mX.getFlingState() == Axis.FlingStates.WAITING_TO_SNAP; + boolean waitingToSnapY = mY.getFlingState() == Axis.FlingStates.WAITING_TO_SNAP; + if ((mX.getOverscroll() == Axis.Overscroll.PLUS || mX.getOverscroll() == Axis.Overscroll.MINUS) && + (mY.getOverscroll() == Axis.Overscroll.PLUS || mY.getOverscroll() == Axis.Overscroll.MINUS)) + { + if (waitingToSnapX && waitingToSnapY) { + mX.startSnap(); mY.startSnap(); + } + } else { + if (waitingToSnapX) + mX.startSnap(); + if (waitingToSnapY) + mY.startSnap(); } - } else { - if (waitingToSnapX) - mX.startSnap(); - if (waitingToSnapY) - mY.startSnap(); } mX.displace(); mY.displace(); updatePosition(); if (mX.getFlingState() == Axis.FlingStates.STOPPED && mY.getFlingState() == Axis.FlingStates.STOPPED) { stop(); @@ -532,26 +591,30 @@ public class PanZoomController BOTH, // Overscrolled in both directions (page is zoomed to smaller than screen) } public float firstTouchPos; /* Position of the first touch event on the current drag. */ public float touchPos; /* Position of the most recent touch event on the current drag. */ public float lastTouchPos; /* Position of the touch event before touchPos. */ public float velocity; /* Velocity in this direction. */ public boolean locked; /* Whether movement on this axis is locked. */ + public boolean disableSnap; /* Whether overscroll snapping is disabled. */ private FlingStates mFlingState; /* The fling state we're in on this axis. */ private EaseOutAnimation mSnapAnim; /* The animation when the page is snapping back. */ /* These three need to be kept in sync with the layer controller. */ - public float viewportPos; + private float viewportPos; private float mViewportLength; private int mScreenLength; private float mPageLength; + public float displacement; + private float mSnapPosition; + public FlingStates getFlingState() { return mFlingState; } public void setFlingState(FlingStates aFlingState) { mFlingState = aFlingState; } public void setViewportLength(float viewportLength) { mViewportLength = viewportLength; } public void setScreenLength(int screenLength) { mScreenLength = screenLength; } @@ -590,20 +653,18 @@ public class PanZoomController } public void startFling(boolean stopped) { if (!stopped) { setFlingState(FlingStates.FLINGING); return; } - float excess = getExcess(); - if (FloatUtils.fuzzyEquals(excess, 0.0f)) - setFlingState(FlingStates.STOPPED); - else + setFlingState(FlingStates.STOPPED); + if (!disableSnap && !FloatUtils.fuzzyEquals(getExcess(), 0.0f)) setFlingState(FlingStates.WAITING_TO_SNAP); } // Advances a fling animation by one step. public void advanceFling() { switch (mFlingState) { case FLINGING: scroll(); @@ -616,17 +677,17 @@ public class PanZoomController return; } } // Performs one frame of a scroll operation if applicable. private void scroll() { // If we aren't overscrolled, just apply friction. float excess = getExcess(); - if (FloatUtils.fuzzyEquals(excess, 0.0f)) { + if (disableSnap || FloatUtils.fuzzyEquals(excess, 0.0f)) { velocity *= FRICTION; if (Math.abs(velocity) < 0.1f) { velocity = 0.0f; setFlingState(FlingStates.STOPPED); } return; } @@ -642,50 +703,53 @@ public class PanZoomController setFlingState(FlingStates.WAITING_TO_SNAP); } } // Starts a snap-into-place operation. public void startSnap() { switch (getOverscroll()) { case MINUS: - mSnapAnim = new EaseOutAnimation(viewportPos, viewportPos + getExcess()); + mSnapAnim = new EaseOutAnimation(0, getExcess()); break; case PLUS: - mSnapAnim = new EaseOutAnimation(viewportPos, viewportPos - getExcess()); + mSnapAnim = new EaseOutAnimation(0, -getExcess()); break; default: // no overscroll to deal with, so we're done setFlingState(FlingStates.STOPPED); return; } + displacement = 0; + mSnapPosition = mSnapAnim.getPosition(); setFlingState(FlingStates.SNAPPING); } // Performs one frame of a snap-into-place operation. private void snap() { mSnapAnim.advance(); - viewportPos = mSnapAnim.getPosition(); + displacement += mSnapAnim.getPosition() - mSnapPosition; + mSnapPosition = mSnapAnim.getPosition(); if (mSnapAnim.getFinished()) { mSnapAnim = null; setFlingState(FlingStates.STOPPED); } } // Performs displacement of the viewport position according to the current velocity. public void displace() { if (locked) return; if (mFlingState == FlingStates.PANNING) - viewportPos += lastTouchPos - touchPos; + displacement += lastTouchPos - touchPos; else - viewportPos += velocity; + displacement += velocity; } } private static class EaseOutAnimation { private float[] mFrames; private float mPosition; private float mOrigin; private float mDest; @@ -779,21 +843,21 @@ public class PanZoomController @Override public void onScaleEnd(ScaleGestureDetector detector) { PointF o = mController.getOrigin(); if (mState == PanZoomState.ANIMATED_ZOOM) return; mState = PanZoomState.PANNING_HOLD_LOCKED; - mX.firstTouchPos = mX.touchPos = detector.getFocusX(); - mY.firstTouchPos = mY.touchPos = detector.getFocusY(); + mX.firstTouchPos = mX.lastTouchPos = mX.touchPos = detector.getFocusX(); + mY.firstTouchPos = mY.lastTouchPos = mY.touchPos = detector.getFocusY(); RectF viewport = mController.getViewport(); - + FloatSize pageSize = mController.getPageSize(); RectF pageRect = new RectF(0,0, pageSize.width, pageSize.height); if (!pageRect.contains(viewport)) { // animatedZoomTo will ensure that our destRect is within the page bounds animatedZoomTo(viewport); } else { // Force a viewport synchronisation diff --git a/mobile/android/chrome/content/browser.js b/mobile/android/chrome/content/browser.js --- a/mobile/android/chrome/content/browser.js +++ b/mobile/android/chrome/content/browser.js @@ -1411,35 +1411,79 @@ var BrowserEventHandler = { window.addEventListener("mousedown", this, true); window.addEventListener("mouseup", this, true); window.addEventListener("mousemove", this, true); Services.obs.addObserver(this, "Gesture:SingleTap", false); Services.obs.addObserver(this, "Gesture:ShowPress", false); Services.obs.addObserver(this, "Gesture:CancelTouch", false); Services.obs.addObserver(this, "Gesture:DoubleTap", false); + Services.obs.addObserver(this, "Gesture:Scroll", false); BrowserApp.deck.addEventListener("DOMContentLoaded", this, true); BrowserApp.deck.addEventListener("DOMLinkAdded", this, true); BrowserApp.deck.addEventListener("DOMTitleChanged", this, true); BrowserApp.deck.addEventListener("DOMUpdatePageReport", PopupBlockerObserver.onUpdatePageReport, false); }, observe: function(aSubject, aTopic, aData) { - if (aTopic == "Gesture:CancelTouch") { + if (aTopic == "Gesture:Scroll") { + // If we've lost our scrollable element, return. Don't cancel the + // override, as we probably don't want Java to handle panning until the + // user releases their finger. + if (this._scrollableElement == null) + return; + + // If this is the first scroll event and we can't scroll in the direction + // the user wanted, and neither can any non-root sub-frame, cancel the + // override so that Java can handle panning the main document. + let data = JSON.parse(aData); + if (this._firstScrollEvent) { + while (this._scrollableElement != null && + !this._elementCanScroll(this._scrollableElement, data.x, data.y)) + this._scrollableElement = this._findScrollableElement(this._scrollableElement, false); + + let doc = BrowserApp.selectedBrowser.contentDocument; + if (this._scrollableElement == doc.body || + this._scrollableElement == doc.documentElement) { + sendMessageToJava({ gecko: { type: "Panning:CancelOverride" } }); + return; + } + + this._firstScrollEvent = false; + } + + // Scroll the scrollable element + this._scrollElementBy(this._scrollableElement, data.x, data.y); + sendMessageToJava({ gecko: { type: "Gesture:ScrollAck" } }); + } else if (aTopic == "Gesture:CancelTouch") { this._cancelTapHighlight(); } else if (aTopic == "Gesture:ShowPress") { let data = JSON.parse(aData); let closest = ElementTouchHelper.elementFromPoint(BrowserApp.selectedBrowser.contentWindow, data.x, data.y); if (!closest) closest = ElementTouchHelper.anyElementFromPoint(BrowserApp.selectedBrowser.contentWindow, data.x, data.y); - if (closest) + if (closest) { this._doTapHighlight(closest); + + // If we've pressed a scrollable element, let Java know that we may + // want to override the scroll behaviour (for document sub-frames) + this._scrollableElement = this._findScrollableElement(closest, true); + this._firstScrollEvent = true; + + if (this._scrollableElement != null) { + // Discard if it's the top-level scrollable, we let Java handle this + let doc = BrowserApp.selectedBrowser.contentDocument; + if (this._scrollableElement != doc.body && + this._scrollableElement != doc.documentElement) + sendMessageToJava({ gecko: { type: "Panning:Override" } }); + } + } } else if (aTopic == "Gesture:SingleTap") { let element = this._highlightElement; if (element && !FormAssistant.handleClick(element)) { let data = JSON.parse(aData); [data.x, data.y] = ElementTouchHelper.toScreenCoords(element.ownerDocument.defaultView, data.x, data.y); this._sendMouseEvent("mousemove", element, data.x, data.y); this._sendMouseEvent("mousedown", element, data.x, data.y); @@ -1493,17 +1537,21 @@ var BrowserEventHandler = { rect.type = "Browser:ZoomToRect"; rect.x -= margin; rect.w += 2*margin; sendMessageToJava({ gecko: rect }); }, 0); } }, - _highlihtElement: null, + _firstScrollEvent: false, + + _scrollableElement: null, + + _highlightElement: null, _doTapHighlight: function _doTapHighlight(aElement) { DOMUtils.setContentState(aElement, kStateActive); this._highlightElement = aElement; }, _cancelTapHighlight: function _cancelTapHighlight() { DOMUtils.setContentState(BrowserApp.selectedBrowser.contentWindow.document.documentElement, kStateActive); @@ -1644,24 +1692,26 @@ var BrowserEventHandler = { _findScrollableElement: function(elem, checkElem) { // Walk the DOM tree until we find a scrollable element let scrollable = false; while (elem) { /* Element is scrollable if its scroll-size exceeds its client size, and: * - It has overflow 'auto' or 'scroll' * - It's a textarea - * We don't consider HTML/BODY nodes here, since Java pans those. + * - It's an HTML/BODY node */ if (checkElem) { if (((elem.scrollHeight > elem.clientHeight) || (elem.scrollWidth > elem.clientWidth)) && (elem.style.overflow == 'auto' || elem.style.overflow == 'scroll' || - elem.localName == 'textarea')) { + elem.localName == 'textarea' || + elem.localName == 'html' || + elem.localName == 'body')) { scrollable = true; break; } } else { checkElem = true; } // Propagate up iFrames