michael@0: /* vim: set ft=javascript ts=2 et sw=2 tw=80: */ michael@0: /* This Source Code Form is subject to the terms of the Mozilla Public michael@0: * License, v. 2.0. If a copy of the MPL was not distributed with this michael@0: * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ michael@0: michael@0: let {CC, Cc, Ci, Cu, Cr} = require('chrome'); michael@0: michael@0: Cu.import('resource://gre/modules/Services.jsm'); michael@0: michael@0: let handlerCount = 0; michael@0: michael@0: let orig_w3c_touch_events = Services.prefs.getIntPref('dom.w3c_touch_events.enabled'); michael@0: michael@0: let trackedWindows = new WeakMap(); michael@0: michael@0: // =================== Touch ==================== michael@0: // Simulate touch events on desktop michael@0: function TouchEventHandler (window) { michael@0: // Returns an already instanciated handler for this window michael@0: let cached = trackedWindows.get(window); michael@0: if (cached) { michael@0: return cached; michael@0: } michael@0: michael@0: let contextMenuTimeout = 0; michael@0: michael@0: michael@0: let threshold = 25; michael@0: try { michael@0: threshold = Services.prefs.getIntPref('ui.dragThresholdX'); michael@0: } catch(e) {} michael@0: michael@0: let delay = 500; michael@0: try { michael@0: delay = Services.prefs.getIntPref('ui.click_hold_context_menus.delay'); michael@0: } catch(e) {} michael@0: michael@0: let TouchEventHandler = { michael@0: enabled: false, michael@0: events: ['mousedown', 'mousemove', 'mouseup'], michael@0: start: function teh_start() { michael@0: if (this.enabled) michael@0: return false; michael@0: this.enabled = true; michael@0: let isReloadNeeded = Services.prefs.getIntPref('dom.w3c_touch_events.enabled') != 1; michael@0: Services.prefs.setIntPref('dom.w3c_touch_events.enabled', 1); michael@0: this.events.forEach((function(evt) { michael@0: // Only listen trusted events to prevent messing with michael@0: // event dispatched manually within content documents michael@0: window.addEventListener(evt, this, true, false); michael@0: }).bind(this)); michael@0: return isReloadNeeded; michael@0: }, michael@0: stop: function teh_stop() { michael@0: if (!this.enabled) michael@0: return; michael@0: this.enabled = false; michael@0: Services.prefs.setIntPref('dom.w3c_touch_events.enabled', orig_w3c_touch_events); michael@0: this.events.forEach((function(evt) { michael@0: window.removeEventListener(evt, this, true); michael@0: }).bind(this)); michael@0: }, michael@0: handleEvent: function teh_handleEvent(evt) { michael@0: // Ignore all but real mouse event coming from physical mouse michael@0: // (especially ignore mouse event being dispatched from a touch event) michael@0: if (evt.button || evt.mozInputSource != Ci.nsIDOMMouseEvent.MOZ_SOURCE_MOUSE || evt.isSynthesized) { michael@0: return; michael@0: } michael@0: michael@0: // The gaia system window use an hybrid system even on the device which is michael@0: // a mix of mouse/touch events. So let's not cancel *all* mouse events michael@0: // if it is the current target. michael@0: let content = this.getContent(evt.target); michael@0: let isSystemWindow = content.location.toString().indexOf("system.gaiamobile.org") != -1; michael@0: michael@0: let eventTarget = this.target; michael@0: let type = ''; michael@0: switch (evt.type) { michael@0: case 'mousedown': michael@0: this.target = evt.target; michael@0: michael@0: contextMenuTimeout = michael@0: this.sendContextMenu(evt.target, evt.pageX, evt.pageY, delay); michael@0: michael@0: this.cancelClick = false; michael@0: this.startX = evt.pageX; michael@0: this.startY = evt.pageY; michael@0: michael@0: // Capture events so if a different window show up the events michael@0: // won't be dispatched to something else. michael@0: evt.target.setCapture(false); michael@0: michael@0: type = 'touchstart'; michael@0: break; michael@0: michael@0: case 'mousemove': michael@0: if (!eventTarget) michael@0: return; michael@0: michael@0: if (!this.cancelClick) { michael@0: if (Math.abs(this.startX - evt.pageX) > threshold || michael@0: Math.abs(this.startY - evt.pageY) > threshold) { michael@0: this.cancelClick = true; michael@0: content.clearTimeout(contextMenuTimeout); michael@0: } michael@0: } michael@0: michael@0: type = 'touchmove'; michael@0: break; michael@0: michael@0: case 'mouseup': michael@0: if (!eventTarget) michael@0: return; michael@0: this.target = null; michael@0: michael@0: content.clearTimeout(contextMenuTimeout); michael@0: type = 'touchend'; michael@0: michael@0: // Only register click listener after mouseup to ensure michael@0: // catching only real user click. (Especially ignore click michael@0: // being dispatched on form submit) michael@0: if (evt.detail == 1) { michael@0: window.addEventListener('click', this, true, false); michael@0: } michael@0: break; michael@0: michael@0: case 'click': michael@0: // Mouse events has been cancelled so dispatch a sequence michael@0: // of events to where touchend has been fired michael@0: evt.preventDefault(); michael@0: evt.stopImmediatePropagation(); michael@0: michael@0: window.removeEventListener('click', this, true, false); michael@0: michael@0: if (this.cancelClick) michael@0: return; michael@0: michael@0: ignoreEvents = true; michael@0: content.setTimeout(function dispatchMouseEvents(self) { michael@0: try { michael@0: self.fireMouseEvent('mousedown', evt); michael@0: self.fireMouseEvent('mousemove', evt); michael@0: self.fireMouseEvent('mouseup', evt); michael@0: } catch(e) { michael@0: Cu.reportError('Exception in touch event helper: ' + e); michael@0: } michael@0: ignoreEvents = false; michael@0: }, 0, this); michael@0: michael@0: return; michael@0: } michael@0: michael@0: let target = eventTarget || this.target; michael@0: if (target && type) { michael@0: this.sendTouchEvent(evt, target, type); michael@0: } michael@0: michael@0: if (!isSystemWindow) { michael@0: evt.preventDefault(); michael@0: evt.stopImmediatePropagation(); michael@0: } michael@0: }, michael@0: fireMouseEvent: function teh_fireMouseEvent(type, evt) { michael@0: let content = this.getContent(evt.target); michael@0: var utils = content.QueryInterface(Ci.nsIInterfaceRequestor) michael@0: .getInterface(Ci.nsIDOMWindowUtils); michael@0: utils.sendMouseEvent(type, evt.clientX, evt.clientY, 0, 1, 0, true, 0, Ci.nsIDOMMouseEvent.MOZ_SOURCE_TOUCH); michael@0: }, michael@0: sendContextMenu: function teh_sendContextMenu(target, x, y, delay) { michael@0: let doc = target.ownerDocument; michael@0: let evt = doc.createEvent('MouseEvent'); michael@0: evt.initMouseEvent('contextmenu', true, true, doc.defaultView, michael@0: 0, x, y, x, y, false, false, false, false, michael@0: 0, null); michael@0: michael@0: let content = this.getContent(target); michael@0: let timeout = content.setTimeout((function contextMenu() { michael@0: target.dispatchEvent(evt); michael@0: this.cancelClick = true; michael@0: }).bind(this), delay); michael@0: michael@0: return timeout; michael@0: }, michael@0: sendTouchEvent: function teh_sendTouchEvent(evt, target, name) { michael@0: // When running OOP b2g desktop, we need to send the touch events michael@0: // using the mozbrowser api on the unwrapped frame. michael@0: if (target.localName == "iframe" && target.mozbrowser === true) { michael@0: if (name == "touchstart") { michael@0: this.touchstartTime = Date.now(); michael@0: } else if (name == "touchend") { michael@0: // If we have a 'fast' tap, don't send a click as both will be turned michael@0: // into a click and that breaks eg. checkboxes. michael@0: if (Date.now() - this.touchstartTime < delay) { michael@0: this.cancelClick = true; michael@0: } michael@0: } michael@0: let unwraped = XPCNativeWrapper.unwrap(target); michael@0: unwraped.sendTouchEvent(name, [0], // event type, id michael@0: [evt.clientX], [evt.clientY], // x, y michael@0: [1], [1], // rx, ry michael@0: [0], [0], // rotation, force michael@0: 1); // count michael@0: return; michael@0: } michael@0: let document = target.ownerDocument; michael@0: let content = this.getContent(target); michael@0: michael@0: let touchEvent = document.createEvent('touchevent'); michael@0: let point = document.createTouch(content, target, 0, michael@0: evt.pageX, evt.pageY, michael@0: evt.screenX, evt.screenY, michael@0: evt.clientX, evt.clientY, michael@0: 1, 1, 0, 0); michael@0: let touches = document.createTouchList(point); michael@0: let targetTouches = touches; michael@0: let changedTouches = touches; michael@0: touchEvent.initTouchEvent(name, true, true, content, 0, michael@0: false, false, false, false, michael@0: touches, targetTouches, changedTouches); michael@0: target.dispatchEvent(touchEvent); michael@0: return touchEvent; michael@0: }, michael@0: getContent: function teh_getContent(target) { michael@0: let win = target.ownerDocument.defaultView; michael@0: return win; michael@0: } michael@0: }; michael@0: trackedWindows.set(window, TouchEventHandler); michael@0: michael@0: return TouchEventHandler; michael@0: } michael@0: michael@0: exports.TouchEventHandler = TouchEventHandler;