Bug 1416310 - 2. Use per-GeckoView event to handle scroll-to-focused-input; r?rbarker
Instead of sending an event through the global EventDispatcher in
GeckoLayerClient, switch to using the per-GeckoView EventDispatcher in
GeckoInputConnection, to handle scroll-to-focused-input-on-resize. This
lets us implement the same functionality for standalone GeckoView.
The patch also fixes some small bugs including unregistering
not-registered events, not scrolling when switching input focus, and
inadvertent scrolling when not showing the keyboard.
MozReview-Commit-ID: 20OZP9dMXtI
--- a/mobile/android/chrome/content/browser.js
+++ b/mobile/android/chrome/content/browser.js
@@ -359,16 +359,17 @@ var BrowserApp = {
this.deck = document.getElementById("browsers");
BrowserEventHandler.init();
Services.androidBridge.browserApp = this;
WindowEventDispatcher.registerListener(this, [
+ "GeckoView:ZoomToInput",
"Session:Restore",
"Tab:Load",
"Tab:Selected",
"Tab:Closed",
"Tab:Move",
"Tab:OpenUri",
]);
@@ -378,17 +379,16 @@ var BrowserApp = {
"Fonts:Reload",
"FormHistory:Init",
"FullScreen:Exit",
"Locale:OS",
"Locale:Changed",
"Passwords:Init",
"Sanitize:ClearData",
"SaveAs:PDF",
- "ScrollTo:FocusedInput",
"Session:Back",
"Session:Forward",
"Session:GetHistory",
"Session:Navigate",
"Session:Reload",
"Session:Stop",
"Telemetry:CustomTabsPing",
]);
@@ -1605,59 +1605,59 @@ var BrowserApp = {
// results in a better user experience
focused = focused.ownerGlobal.frameElement;
}
return focused;
}
return null;
},
- scrollToFocusedInput: function(aBrowser, aAllowZoom = true) {
+ scrollToFocusedInput: function(aBrowser) {
let formHelperMode = Services.prefs.getIntPref("formhelper.mode");
- if (formHelperMode == kFormHelperModeDisabled)
+ if (formHelperMode == kFormHelperModeDisabled) {
return;
-
- let dwu = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor).getInterface(Ci.nsIDOMWindowUtils);
+ }
+
+ let dwu = aBrowser.contentWindow.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
if (!dwu) {
return;
}
- let apzFlushDone = function() {
- Services.obs.removeObserver(apzFlushDone, "apz-repaints-flushed");
- dwu.zoomToFocusedInput();
+ let zoomToFocusedInput = function() {
+ if (!dwu.flushApzRepaints()) {
+ dwu.zoomToFocusedInput();
+ return;
+ }
+ Services.obs.addObserver(function apzFlushDone() {
+ Services.obs.removeObserver(apzFlushDone, "apz-repaints-flushed");
+ dwu.zoomToFocusedInput();
+ }, "apz-repaints-flushed");
};
- let paintDone = function() {
- window.removeEventListener("MozAfterPaint", paintDone);
- if (dwu.flushApzRepaints()) {
- Services.obs.addObserver(apzFlushDone, "apz-repaints-flushed");
+ let gotResize = false;
+ let onResize = function() {
+ gotResize = true;
+ if (dwu.isMozAfterPaintPending) {
+ addEventListener("MozAfterPaint", zoomToFocusedInput, {once: true});
} else {
- apzFlushDone();
+ zoomToFocusedInput();
}
};
- let gotResizeWindow = false;
- let resizeWindow = function(e) {
- gotResizeWindow = true;
- aBrowser.contentWindow.removeEventListener("resize", resizeWindow);
- if (dwu.isMozAfterPaintPending) {
- window.addEventListener("MozAfterPaint", paintDone);
- } else {
- paintDone();
- }
- }
-
- aBrowser.contentWindow.addEventListener("resize", resizeWindow);
-
- // The "resize" event sometimes fails to fire, so set a timer to catch that case
- // and unregister the event listener. See Bug 1253469
+ aBrowser.contentWindow.addEventListener("resize", onResize);
+
+ // When the keyboard is displayed, we can get one resize event, multiple
+ // resize events, or none at all. Try to handle all these cases by allowing
+ // resizing within a set interval, and still zoom to input if there is no
+ // resize event at the end of the interval.
setTimeout(function(e) {
- if (!gotResizeWindow) {
- aBrowser.contentWindow.removeEventListener("resize", resizeWindow);
- dwu.zoomToFocusedInput();
+ aBrowser.contentWindow.removeEventListener("resize", onResize);
+ if (!gotResize) {
+ onResize();
}
}, 500);
},
getUALocalePref: function () {
return Services.locale.getRequestedLocale() || undefined;
},
@@ -1764,20 +1764,20 @@ var BrowserApp = {
case "Sanitize:ClearData":
this.sanitize(data);
break;
case "SaveAs:PDF":
this.saveAsPDF(browser);
break;
- case "ScrollTo:FocusedInput": {
+ case "GeckoView:ZoomToInput": {
// these messages come from a change in the viewable area and not user interaction
// we allow scrolling to the selected input, but not zooming the page
- this.scrollToFocusedInput(browser, false);
+ this.scrollToFocusedInput(browser);
break;
}
case "Telemetry:CustomTabsPing": {
TelemetryController.submitExternalPing("anonymous", { client: data.client }, { addClientId: false });
break;
}
--- a/mobile/android/chrome/geckoview/GeckoViewContent.js
+++ b/mobile/android/chrome/geckoview/GeckoViewContent.js
@@ -3,16 +3,20 @@
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
const { classes: Cc, interfaces: Ci, utils: Cu } = Components;
Cu.import("resource://gre/modules/GeckoViewContentModule.jsm");
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+XPCOMUtils.defineLazyModuleGetters(this, {
+ Services: "resource://gre/modules/Services.jsm",
+});
+
XPCOMUtils.defineLazyGetter(this, "dump", () =>
Cu.import("resource://gre/modules/AndroidLog.jsm",
{}).AndroidLog.d.bind(null, "ViewContent"));
function debug(aMsg) {
// dump(aMsg);
}
@@ -20,58 +24,106 @@ class GeckoViewContent extends GeckoView
register() {
debug("register");
addEventListener("DOMTitleChanged", this, false);
addEventListener("MozDOMFullscreen:Entered", this, false);
addEventListener("MozDOMFullscreen:Exit", this, false);
addEventListener("MozDOMFullscreen:Exited", this, false);
addEventListener("MozDOMFullscreen:Request", this, false);
- addEventListener("contextmenu", this, { capture: true, passive: false });
+ addEventListener("contextmenu", this, { capture: true });
this.messageManager.addMessageListener("GeckoView:DOMFullscreenEntered",
this);
this.messageManager.addMessageListener("GeckoView:DOMFullscreenExited",
this);
+ this.messageManager.addMessageListener("GeckoView:ZoomToInput",
+ this);
}
unregister() {
debug("unregister");
removeEventListener("DOMTitleChanged", this);
removeEventListener("MozDOMFullscreen:Entered", this);
removeEventListener("MozDOMFullscreen:Exit", this);
removeEventListener("MozDOMFullscreen:Exited", this);
removeEventListener("MozDOMFullscreen:Request", this);
- removeEventListener("contextmenu", this);
+ removeEventListener("contextmenu", this, { capture: true });
this.messageManager.removeMessageListener("GeckoView:DOMFullscreenEntered",
this);
this.messageManager.removeMessageListener("GeckoView:DOMFullscreenExited",
this);
+ this.messageManager.removeMessageListener("GeckoView:ZoomToInput",
+ this);
}
receiveMessage(aMsg) {
debug("receiveMessage " + aMsg.name);
switch (aMsg.name) {
case "GeckoView:DOMFullscreenEntered":
if (content) {
content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.handleFullscreenRequests();
}
break;
+
case "GeckoView:DOMFullscreenExited":
if (content) {
content.QueryInterface(Ci.nsIInterfaceRequestor)
.getInterface(Ci.nsIDOMWindowUtils)
.exitFullscreen();
}
break;
+
+ case "GeckoView:ZoomToInput": {
+ let dwu = content.QueryInterface(Ci.nsIInterfaceRequestor)
+ .getInterface(Ci.nsIDOMWindowUtils);
+
+ let zoomToFocusedInput = function() {
+ if (!dwu.flushApzRepaints()) {
+ dwu.zoomToFocusedInput();
+ return;
+ }
+ Services.obs.addObserver(function apzFlushDone() {
+ Services.obs.removeObserver(apzFlushDone, "apz-repaints-flushed");
+ dwu.zoomToFocusedInput();
+ }, "apz-repaints-flushed");
+ };
+
+ let gotResize = false;
+ let onResize = function() {
+ gotResize = true;
+ if (dwu.isMozAfterPaintPending) {
+ addEventListener("MozAfterPaint", function paintDone() {
+ removeEventListener("MozAfterPaint", paintDone, {capture: true});
+ zoomToFocusedInput();
+ }, {capture: true});
+ } else {
+ zoomToFocusedInput();
+ }
+ };
+
+ addEventListener("resize", onResize, { capture: true });
+
+ // When the keyboard is displayed, we can get one resize event,
+ // multiple resize events, or none at all. Try to handle all these
+ // cases by allowing resizing within a set interval, and still zoom to
+ // input if there is no resize event at the end of the interval.
+ content.setTimeout(() => {
+ removeEventListener("resize", onResize, { capture: true });
+ if (!gotResize) {
+ onResize();
+ }
+ }, 500);
+ }
+ break;
}
}
handleEvent(aEvent) {
debug("handleEvent " + aEvent.type);
switch (aEvent.type) {
case "contextmenu":
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/GeckoInputConnection.java
@@ -230,18 +230,21 @@ class GeckoInputConnection
public void run() {
if (v.hasFocus() && !imm.isActive(v)) {
// Marshmallow workaround: The view has focus but it is not the active
// view for the input method. (Bug 1211848)
v.clearFocus();
v.requestFocus();
}
final GeckoView view = getView();
- if (view != null && showToolbar) {
- view.getDynamicToolbarAnimator().showToolbar(/*immediately*/ true);
+ if (view != null) {
+ if (showToolbar) {
+ view.getDynamicToolbarAnimator().showToolbar(/*immediately*/ true);
+ }
+ view.getEventDispatcher().dispatch("GeckoView:ZoomToInput", null);
}
mSoftInputReentrancyGuard = true;
imm.showSoftInput(v, 0);
mSoftInputReentrancyGuard = false;
}
});
}
--- a/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
+++ b/mobile/android/geckoview/src/main/java/org/mozilla/gecko/gfx/GeckoLayerClient.java
@@ -2,17 +2,16 @@
* This Source Code Form is subject to the terms of the Mozilla Public
* License, v. 2.0. If a copy of the MPL was not distributed with this
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
package org.mozilla.gecko.gfx;
import org.mozilla.gecko.annotation.RobocopTarget;
import org.mozilla.gecko.annotation.WrapForJNI;
-import org.mozilla.gecko.EventDispatcher;
import org.mozilla.gecko.util.GeckoBundle;
import android.graphics.Color;
import android.graphics.Matrix;
import android.graphics.PointF;
import android.graphics.RectF;
import android.os.SystemClock;
import android.util.Log;
@@ -23,17 +22,16 @@ import java.util.ArrayList;
class GeckoLayerClient implements LayerView.Listener
{
private static final String LOGTAG = "GeckoLayerClient";
private IntSize mWindowSize;
private boolean mForceRedraw;
- private boolean mImeWasEnabledOnLastResize;
/* The current viewport metrics.
* This is volatile so that we can read and write to it from different threads.
* We avoid synchronization to make getting the viewport metrics from
* the compositor as cheap as possible. The viewport is immutable so
* we don't need to worry about anyone mutating it while we're reading from it.
* Specifically:
* 1) reading mViewportMetrics from any thread is fine without synchronization
@@ -100,29 +98,16 @@ class GeckoLayerClient implements LayerV
* result in an infinite loop.
*/
boolean setViewportSize(int width, int height) {
if (mViewportMetrics.viewportRectWidth == width &&
mViewportMetrics.viewportRectHeight == height) {
return false;
}
mViewportMetrics = mViewportMetrics.setViewportSize(width, height);
-
- if (mView.isCompositorReady()) {
- // the following call also sends gecko a message, which will be processed after the resize
- // message above has updated the viewport. this message ensures that if we have just put
- // focus in a text field, we scroll the content so that the text field is in view.
- final boolean imeIsEnabled = mView.isIMEEnabled();
- if (imeIsEnabled && !mImeWasEnabledOnLastResize) {
- // The IME just came up after not being up, so let's scroll
- // to the focused input.
- EventDispatcher.getInstance().dispatch("ScrollTo:FocusedInput", null);
- }
- mImeWasEnabledOnLastResize = imeIsEnabled;
- }
return true;
}
PanZoomController getPanZoomController() {
return mPanZoomController;
}
DynamicToolbarAnimator getDynamicToolbarAnimator() {
--- a/mobile/android/geckoview_example/src/main/AndroidManifest.xml
+++ b/mobile/android/geckoview_example/src/main/AndroidManifest.xml
@@ -4,16 +4,17 @@
<application android:allowBackup="true"
android:label="@string/app_name"
android:supportsRtl="true">
<uses-library android:name="android.test.runner" />
<activity android:name="org.mozilla.geckoview_example.GeckoViewActivity"
android:label="GeckoViewActivity"
+ android:windowSoftInputMode="stateUnspecified|adjustResize"
android:launchMode="singleTop">
<intent-filter>
<action android:name="android.intent.action.MAIN" />
<category android:name="android.intent.category.LAUNCHER" />
<category android:name="android.intent.category.MULTIWINDOW_LAUNCHER"/>
<category android:name="android.intent.category.APP_BROWSER" />
<category android:name="android.intent.category.DEFAULT" />
</intent-filter>
--- a/mobile/android/modules/geckoview/GeckoViewContent.jsm
+++ b/mobile/android/modules/geckoview/GeckoViewContent.jsm
@@ -34,40 +34,50 @@ class GeckoViewContent extends GeckoView
this.frameScriptLoaded = true;
}
this.window.addEventListener("MozDOMFullScreen:Entered", this,
/* capture */ true, /* untrusted */ false);
this.window.addEventListener("MozDOMFullScreen:Exited", this,
/* capture */ true, /* untrusted */ false);
- this.eventDispatcher.registerListener(this, "GeckoViewContent:ExitFullScreen");
+ this.eventDispatcher.registerListener(this, [
+ "GeckoViewContent:ExitFullScreen",
+ "GeckoView:ZoomToInput",
+ ]);
+
this.messageManager.addMessageListener("GeckoView:DOMFullscreenExit", this);
this.messageManager.addMessageListener("GeckoView:DOMFullscreenRequest", this);
}
// Bundle event handler.
onEvent(aEvent, aData, aCallback) {
debug("onEvent: " + aEvent);
switch (aEvent) {
case "GeckoViewContent:ExitFullScreen":
- this.messageManager.sendAsyncMessage("GeckoView:DOMFullscreenExited");
+ case "GeckoView:ZoomToInput":
+ this.messageManager.sendAsyncMessage(aEvent);
break;
case "GeckoView:SetActive":
this.browser.docShellIsActive = aData.active;
break;
}
}
unregister() {
this.window.removeEventListener("MozDOMFullScreen:Entered", this,
/* capture */ true);
this.window.removeEventListener("MozDOMFullScreen:Exited", this,
/* capture */ true);
- this.eventDispatcher.unregisterListener(this, "GeckoViewContent:ExitFullScreen");
+
+ this.eventDispatcher.unregisterListener(this, [
+ "GeckoViewContent:ExitFullScreen",
+ "GeckoView:ZoomToInput",
+ ]);
+
this.messageManager.removeMessageListener("GeckoView:DOMFullscreenExit", this);
this.messageManager.removeMessageListener("GeckoView:DOMFullscreenRequest", this);
}
// DOM event handler
handleEvent(aEvent) {
debug("handleEvent: aEvent.type=" + aEvent.type);