gecko/toolkit/devtools/touch-events.js

281 lines
9.8 KiB
JavaScript

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