From 1c4760b2d36dc4267712d53839e9ee79c1ee49ac Mon Sep 17 00:00:00 2001 From: Wes Johnston Date: Mon, 21 Mar 2011 11:20:34 -0700 Subject: [PATCH] Bug 544614 - Touch Events [r=mfinkle, bstover] --- mobile/app/mobile.js | 6 +- mobile/chrome/content/browser.js | 124 ++++++++++++++++++++----------- mobile/chrome/content/content.js | 70 ++++++++++++++++- mobile/chrome/content/input.js | 10 ++- 4 files changed, 162 insertions(+), 48 deletions(-) diff --git a/mobile/app/mobile.js b/mobile/app/mobile.js index b9a589e00c86..bd4e098b272e 100644 --- a/mobile/app/mobile.js +++ b/mobile/app/mobile.js @@ -584,6 +584,9 @@ pref("image.mem.decodeondraw", true); pref("content.image.allow_locking", false); pref("image.mem.min_discard_timeout_ms", 20000); +// enable touch events interfaces +pref("dom.w3c_touch_events.enabled", true); + #ifdef MOZ_SAFE_BROWSING // Safe browsing does nothing unless this pref is set pref("browser.safebrowsing.enabled", true); @@ -633,4 +636,5 @@ pref("urlclassifier.updatecachemax", 4194304); // URL for checking the reason for a malware warning. pref("browser.safebrowsing.malware.reportURL", "http://safebrowsing.clients.google.com/safebrowsing/diagnostic?client=%NAME%&hl=%LOCALE%&site="); -#endif \ No newline at end of file +#endif + diff --git a/mobile/chrome/content/browser.js b/mobile/chrome/content/browser.js index 7af5edad24ea..4057324f3743 100644 --- a/mobile/chrome/content/browser.js +++ b/mobile/chrome/content/browser.js @@ -1197,6 +1197,10 @@ Browser.MainDragger = function MainDragger() { Elements.browsers.addEventListener("PanBegin", this, false); Elements.browsers.addEventListener("PanFinished", this, false); + + // allow pages to to override panning, but should + // still allow the sidebars to be panned out of view + this.contentMouseCapture = false; }; Browser.MainDragger.prototype = { @@ -1227,48 +1231,42 @@ Browser.MainDragger.prototype = { dragMove: function dragMove(dx, dy, scroller, aIsKinetic) { let doffset = new Point(dx, dy); - // First calculate any panning to take sidebars out of view - let panOffset = this._panControlsAwayOffset(doffset); + // If the sidebars are showing, we pan them out of the way before panning the content. + // The panning distance that should be used for the sidebars in is stored in sidebarOffset, + // and subtracted from doffset + let sidebarOffset = this._getSidebarOffset(doffset); - // If we started at one sidebar, stop when we get to the other. - if (panOffset.x != 0 && !this._stopAtSidebar) { - this._stopAtSidebar = panOffset.x; // negative: stop at left; positive: stop at right + // If we started with one sidebar open, stop when we get to the other. + if (sidebarOffset.x != 0) + this._blockSidebars(sidebarOffset); + + if (!this.contentMouseCapture) + this._panContent(doffset); + + if (this._hitSidebar && aIsKinetic) + return false; // No kinetic panning after we've stopped at the sidebar. + + // allow panning the sidebars if the page hasn't prevented it, or if any of the sidebars are showing + // (i.e. we always allow panning sidebars off screen but not necessarily panning them back on) + if (!this.contentMouseCapture || sidebarOffset.x != 0 || sidebarOffset.y > 0) + this._panChrome(doffset, sidebarOffset); + + this._updateScrollbars(); + + return !doffset.equals(dx, dy); + }, + + _blockSidebars: function md_blockSidebars(aSidebarOffset) { + // only call this code once + if (!this._stopAtSidebar) { + this._stopAtSidebar = aSidebarOffset.x; // negative: stop at left; positive: stop at right + + // after a timeout, we allow showing the sidebar, to give the appearance of some "friction" at the edge this._sidebarTimeout = setTimeout(function(self) { self._stopAtSidebar = 0; self._sidebarTimeout = null; }, 350, this); } - - if (this._contentView && !this._contentView.isRoot()) { - this._panContentView(this._contentView, doffset); - // XXX we may need to have "escape borders" for iframe panning - // XXX does not deal with scrollables within scrollables - } - - // Do content panning - this._panContentView(getBrowser().getRootView(), doffset); - - if (this._hitSidebar && aIsKinetic) - return; // No kinetic panning after we've stopped at the sidebar. - - // Any leftover panning in doffset would bring controls into view. Add to sidebar - // away panning for the total scroll offset. - let offsetX = doffset.x; - if ((this._stopAtSidebar > 0 && offsetX > 0) || - (this._stopAtSidebar < 0 && offsetX < 0)) { - if (offsetX != panOffset.x) - this._hitSidebar = true; - doffset.x = panOffset.x; - } else { - doffset.add(panOffset); - } - - Browser.tryFloatToolbar(doffset.x, 0); - this._panScroller(Browser.controlsScrollboxScroller, doffset); - this._panScroller(Browser.pageScrollboxScroller, doffset); - this._updateScrollbars(); - - return !doffset.equals(dx, dy); }, handleEvent: function handleEvent(aEvent) { @@ -1299,8 +1297,38 @@ Browser.MainDragger.prototype = { } }, + _panContent: function md_panContent(aOffset) { + if (this._contentView && !this._contentView.isRoot()) { + this._panContentView(this._contentView, aOffset); + // XXX we may need to have "escape borders" for iframe panning + // XXX does not deal with scrollables within scrollables + } + // Do content panning + this._panContentView(getBrowser().getRootView(), aOffset); + }, + + _panChrome: function md_panSidebars(aOffset, aSidebarOffset) { + // Any panning aOffset would bring controls into view. Add to aSidebarOffset + let offsetX = aOffset.x; + if ((this._stopAtSidebar > 0 && offsetX > 0) || + (this._stopAtSidebar < 0 && offsetX < 0)) { + if (offsetX != aSidebarOffset.x) + this._hitSidebar = true; + aOffset.x = aSidebarOffset.x; + } else { + aOffset.add(aSidebarOffset); + } + + Browser.tryFloatToolbar(aOffset.x, 0); + + // pan the sidebars + this._panScroller(Browser.controlsScrollboxScroller, aOffset); + // pan the urlbar + this._panScroller(Browser.pageScrollboxScroller, aOffset); + }, + /** Return offset that pans controls away from screen. Updates doffset with leftovers. */ - _panControlsAwayOffset: function(doffset) { + _getSidebarOffset: function(doffset) { let x = 0, y = 0, rect; rect = Rect.fromRect(Browser.pageScrollbox.getBoundingClientRect()).map(Math.round); @@ -1603,6 +1631,7 @@ const ContentTouchHandler = { document.addEventListener("TapSingle", this, false); document.addEventListener("TapDouble", this, false); document.addEventListener("TapLong", this, false); + document.addEventListener("TapMove", this, false); document.addEventListener("PanBegin", this, false); document.addEventListener("PopupChanged", this, false); @@ -1619,8 +1648,8 @@ const ContentTouchHandler = { // a long tap, without waiting for child process. // messageManager.addMessageListener("Browser:ContextMenu", this); - messageManager.addMessageListener("Browser:Highlight", this); + messageManager.addMessageListener("Browser:CaptureEvents", this); }, handleEvent: function handleEvent(aEvent) { @@ -1661,12 +1690,12 @@ const ContentTouchHandler = { this.tapSingle(aEvent.clientX, aEvent.clientY, aEvent.modifiers); aEvent.preventDefault(); } - } else { - this.tapUp(aEvent.clientX, aEvent.clientY); } + this._dispatchMouseEvent("Browser:MouseUp", aEvent.clientX, aEvent.clientY); break; case "TapSingle": this.tapSingle(aEvent.clientX, aEvent.clientY, aEvent.modifiers); + this._dispatchMouseEvent("Browser:MouseUp", aEvent.clientX, aEvent.clientY); break; case "TapDouble": this.tapDouble(aEvent.clientX, aEvent.clientY, aEvent.modifiers); @@ -1674,6 +1703,9 @@ const ContentTouchHandler = { case "TapLong": this.tapLong(aEvent.clientX, aEvent.clientY); break; + case "TapMove": + this.tapMove(aEvent.clientX, aEvent.clientY); + break; } } } @@ -1694,6 +1726,9 @@ const ContentTouchHandler = { document.dispatchEvent(event); } break; + case "Browser:CaptureEvents": + Elements.browsers.customDragger.contentMouseCapture = aMessage.json.panning; + break; } }, @@ -1735,6 +1770,7 @@ const ContentTouchHandler = { try { fl.activateRemoteFrame(); } catch (e) {} + Elements.browsers.customDragger.contentMouseCapture = false; this._dispatchMouseEvent("Browser:MouseDown", aX, aY); }, @@ -1750,7 +1786,11 @@ const ContentTouchHandler = { tapSingle: function tapSingle(aX, aY, aModifiers) { // Cancel the mouse click if we are showing a context menu if (!ContextHelper.popupState) - this._dispatchMouseEvent("Browser:MouseUp", aX, aY, { modifiers: aModifiers }); + this._dispatchMouseEvent("Browser:MouseClick", aX, aY, { modifiers: aModifiers }); + }, + + tapMove: function tapMove(aX, aY) { + this._dispatchMouseEvent("Browser:MouseMove", aX, aY); }, tapDouble: function tapDouble(aX, aY, aModifiers) { @@ -2733,7 +2773,7 @@ Tab.prototype = { // If the scale level has not changed we want to be sure the content // render correctly since the page refresh process could have been // stalled during page load. In this case if the page has the exact - // same width (like the same page, so by doing 'refresh') and the + // same width (like the same page, so by doing 'refresh') and the // page was scrolled the content is just checkerboard at this point // and this call ensure we render it correctly. browser.getRootView()._updateCacheViewport(); diff --git a/mobile/chrome/content/content.js b/mobile/chrome/content/content.js index 0a33f3081c1c..563689e9974f 100644 --- a/mobile/chrome/content/content.js +++ b/mobile/chrome/content/content.js @@ -262,7 +262,7 @@ let Content = { addMessageListener("Browser:MouseOver", this); addMessageListener("Browser:MouseLong", this); addMessageListener("Browser:MouseDown", this); - addMessageListener("Browser:MouseUp", this); + addMessageListener("Browser:MouseClick", this); addMessageListener("Browser:MouseCancel", this); addMessageListener("Browser:SaveAs", this); addMessageListener("Browser:ZoomToPoint", this); @@ -474,15 +474,17 @@ let Content = { ContextHandler.messageId = json.messageId; - let event = content.document.createEvent("PopupEvents"); - event.initEvent("contextmenu", true, true); + let event = content.document.createEvent("MouseEvent"); + event.initMouseEvent("contextmenu", true, true, content, + 0, x, y, x, y, false, false, false, false, + 0, null); event.x = x; event.y = y; element.dispatchEvent(event); break; } - case "Browser:MouseUp": { + case "Browser:MouseClick": { this._formAssistant.focusSync = true; let element = elementFromPoint(x, y); if (modifiers == Ci.nsIDOMNSEvent.CONTROL_MASK) { @@ -1188,3 +1190,63 @@ var ConsoleAPIObserver = { }; ConsoleAPIObserver.init(); + +var TouchEventHandler = { + element: null, + init: function() { + addMessageListener("Browser:MouseUp", this); + addMessageListener("Browser:MouseDown", this); + addMessageListener("Browser:MouseMove", this); + }, + + receiveMessage: function(aMessage) { + if (Util.isParentProcess()) + return; + + let json = aMessage.json; + let cancelled = false; + + switch (aMessage.name) { + case "Browser:MouseDown": + let cwu = Util.getWindowUtils(content); + this.element = cwu.elementFromPoint(json.x, json.y, false, false); + cancelled = !this.sendEvent("touchstart", json, this.element); + break; + + case "Browser:MouseUp": + if (this.element) + this.sendEvent("touchend", json, this.element); + this.element = null; + break; + + case "Browser:MouseMove": + if (this.element) + cancelled = !this.sendEvent("touchmove", json, this.element); + break; + } + + if (cancelled) + sendAsyncMessage("Browser:CaptureEvents", { messageId: json.messageId, + panning: true }); + }, + + sendEvent: function(aName, aData, aElement) { + if (!Services.prefs.getBoolPref("dom.w3c_touch_events.enabled")) + return true; + + let evt = content.document.createEvent("touchevent"); + let point = content.document.createTouch(content, aElement, 0, + aData.x, aData.y, aData.x, aData.y, aData.x, aData.y, + 1, 1, 0, 0); + let touches = content.document.createTouchList(point); + if (aName == "touchend") { + let empty = content.document.createTouchList(); + evt.initTouchEvent(aName, true, true, content, 0, true, true, true, true, empty, empty, touches); + } else { + evt.initTouchEvent(aName, true, true, content, 0, true, true, true, true, touches, touches, touches); + } + return aElement.dispatchEvent(evt); + } +} + +TouchEventHandler.init(); diff --git a/mobile/chrome/content/input.js b/mobile/chrome/content/input.js index d4b58d01cd93..91ee439fbd5e 100644 --- a/mobile/chrome/content/input.js +++ b/mobile/chrome/content/input.js @@ -335,6 +335,7 @@ MouseModule.prototype = { this.dY += dragData.prevPanY - sY; if (dragData.isPan()) { + this.sendMove(aEvent.clientX, aEvent.clientY, aEvent.target); // Only pan when mouse event isn't part of a click. Prevent jittering on tap. this._kinetic.addData(sX - dragData.prevPanX, sY - dragData.prevPanY); this._dragBy(this.dX, this.dY); @@ -360,6 +361,14 @@ MouseModule.prototype = { } }, + sendMove: function(aX, aY, aTarget) { + let event = document.createEvent("Events"); + event.initEvent("TapMove", true, true); + event.clientX = aX; + event.clientY = aY; + aTarget.dispatchEvent(event); + }, + /** * Inform our dragger of a dragStart. */ @@ -1246,4 +1255,3 @@ GestureModule.prototype = { return r0.translate(offsetX, offsetY); } }; -