gecko/mobile/chrome/content/deckbrowser.xml
2008-09-23 20:02:45 -04:00

1255 lines
43 KiB
XML

<?xml version="1.0"?>
<!DOCTYPE bindings PUBLIC "-//MOZILLA//DTD XBL V1.0//EN" "http://www.mozilla.org/xbl">
<bindings
xmlns="http://www.mozilla.org/xbl"
xmlns:xbl="http://www.mozilla.org/xbl"
xmlns:html="http://www.w3.org/1999/xhtml"
xmlns:xul="http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul">
<binding id="deckbrowser">
<content>
<xul:deck flex="1" selectedIndex="0">
<xul:stack anonid="cstack" flex="1" style="overflow: hidden;">
<html:canvas anonid="ccanvas"
moz-opaque="true"
style="-moz-stack-sizing: ignore;"/>
</xul:stack>
<xul:deck anonid="display-list" flex="1"/>
</xul:deck>
</content>
<resources>
<stylesheet src="chrome://global/content/deckbrowser.css"/>
</resources>
<implementation implements="nsIObserver">
<constructor><![CDATA[
this._zoomLevel = 1;
// panning
this._stack.addEventListener("mousedown", this.stackEventHandler, true);
// need mouseup handled on the window to catch mouseups on e.g. the toolbar
window.addEventListener("mouseup", this.stackEventHandler, true);
this._stack.addEventListener("mousemove", this.stackEventHandler, true);
var prefsvc = Components.classes["@mozilla.org/preferences-service;1"].
getService(Components.interfaces.nsIPrefBranch2);
this._allowKinetic = prefsvc.getBoolPref("browser.ui.panning.kinetic");
// zoom
// FIXME: dblclicks don't work on the device
// this._stack.addEventListener("dblclick", this.stackEventHandler, true);
this._stack.addEventListener("DOMMouseScroll", this.stackEventHandler, true);
var self = this;
var obs = Components.classes["@mozilla.org/observer-service;1"].
getService(Components.interfaces.nsIObserverService);
obs.addObserver(function(subject, topic, data) self.destroyEarliestBrowser(),
"memory-pressure", false);
this._dragStartTimeout = -1;
this.PAN_EVENTS_TO_TRACK = 2;
this._panEventTracker = new Array(this.PAN_EVENTS_TO_TRACK);
this._panEventTrackerIndex = 0;
]]></constructor>
<property name="dragData" readonly="true">
<getter>
<![CDATA[
if (!this.currentTab.dragData) {
this.currentTab.dragData = {
dragging: false,
dragX: 0,
dragY: 0,
sX: 0,
sY: 0,
pageX: 0,
pageY: 0,
oldPageX: 0,
oldPageY: 0
}
}
return this.currentTab.dragData;
]]>
</getter>
</property>
<field name="_stack">
document.getAnonymousElementByAttribute(this, "anonid", "cstack");
</field>
<field name="_canvas">
document.getAnonymousElementByAttribute(this, "anonid", "ccanvas");
</field>
<property name="browser" readonly="true">
<getter>
<![CDATA[
return this.getBrowserForDisplay(this.displayList.selectedPanel);
]]>
</getter>
</property>
<property name="displayList" readonly="true">
<getter>
return document.getAnonymousElementByAttribute(this, "anonid", "display-list");
</getter>
</property>
<field name="tabList">
null
</field>
<field name="progressListenerCreator"/>
<method name="updateCanvasState">
<body><![CDATA[
this._updateViewState();
// clear the whole canvas and redraw
var ctx = this._canvas.getContext("2d");
ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
this._browserToCanvas();
]]></body>
</method>
<method name="_updateViewState">
<body><![CDATA[
// Reset the pan data.
this.dragData.pageX = 0;
this.dragData.pageY = 0;
this._zoomed = false;
this.zoomToPage();
]]></body>
</method>
<field name="currentTab"/>
<method name="getDisplayForTab">
<parameter name="tab"/>
<body><![CDATA[
var tabList = this.tabList.childNodes;
for (var t = 0; t < tabList.length; t++) {
if (tab == tabList[t])
return this.displayList.childNodes[t];
}
return null;
]]>
</body>
</method>
<method name="getTabForDisplay">
<parameter name="display"/>
<body><![CDATA[
var displayList = this.displayList.childNodes;
for (var t = 0; t < displayList.length; t++) {
if (display == displayList[t])
return this.tabList.childNodes[t];
}
return null;
]]>
</body>
</method>
<method name="getBrowserForDisplay">
<parameter name="display"/>
<body><![CDATA[
if (!display)
return null;
var browser = display.firstChild;
if (browser && browser.localName == "browser")
return browser;
browser = display.lastChild;
return (browser && browser.localName == "browser") ? browser : null;
]]>
</body>
</method>
<method name="setLoading">
<parameter name="browser"/>
<body><![CDATA[
var tab = this.getTabForDisplay(browser.parentNode);
if (tab)
tab.dragData = null;
]]></body>
</method>
<method name="updateBrowser">
<parameter name="browser"/>
<parameter name="done"/>
<body><![CDATA[
var display = browser.parentNode;
var domWin = browser.contentWindow;
display.url = domWin.location.toString();
if (!done || domWin.location == "about:blank")
return;
this.restoreBrowserState(display);
var tab = this.getTabForDisplay(display);
if (tab) {
tab.updateTab(browser);
var canvas = display.firstChild;
if (canvas.localName == "canvas")
display.removeChild(canvas);
}
]]></body>
</method>
<field name="browserRedrawHandler">
<![CDATA[
({
deckbrowser: this,
handleEvent: function (aEvent) {
let self = this.deckbrowser;
if (self.dragData.dragging || self.dragData.kineticId)
return;
for (let i = 0; i < aEvent.clientRects.length; i++) {
let e = aEvent.clientRects.item(i);
//dump(Math.floor(e.left) + ", " + Math.floor(e.top) + ", " + Math.ceil(e.width) + ", " + Math.ceil(e.height) + "\n");
self._browserToCanvas2(Math.floor(e.left), Math.floor(e.top), Math.ceil(e.width), Math.ceil(e.height));
}
}
});
]]>
</field>
<method name="selectTab">
<parameter name="tab"/>
<body><![CDATA[
var currentTab = this.currentTab;
this.currentTab = tab;
if (currentTab) {
var currentDisplay = this.getDisplayForTab(currentTab);
var currentBrowser = this.getBrowserForDisplay(currentDisplay);
if (currentBrowser) {
// stop monitor paint events for this browser
currentBrowser.removeEventListener("MozAfterPaint", this.browserRedrawHandler, false);
currentDisplay.url = currentBrowser.contentWindow.location.toString();
currentBrowser.setAttribute("type", "content");
currentTab.updateTab(currentBrowser);
}
}
var display = this.getDisplayForTab(tab);
var browser = this.getBrowserForDisplay(display);
if (!browser) {
browser = this.createBrowser(true, tab, display);
browser.loadURI(display.url, null, null, false);
}
display.lastAccess = Date.now();
browser.setAttribute("type", "content-primary");
this.displayList.selectedPanel = display;
// start monitoring paint events for this browser
browser.addEventListener("MozAfterPaint", this.browserRedrawHandler, false);
// force a repaint of the selected tab
this._browserToCanvas();
var event = document.createEvent("Events");
event.initEvent("TabSelect", true, false);
tab.dispatchEvent(event);
]]></body>
</method>
<method name="newTab">
<parameter name="makeFront"/>
<body><![CDATA[
var browser = this.createBrowser(makeFront, null, null);
if (!browser)
return null;
var tab = this.getTabForDisplay(browser.parentNode);
var evt = document.createEvent("Events");
evt.initEvent("TabOpen", true, false);
tab.dispatchEvent(evt);
return tab;
]]></body>
</method>
<method name="removeTab">
<parameter name="tab"/>
<body><![CDATA[
if (!tab)
return;
var display = this.getDisplayForTab(tab);
if (display)
display.removeChild(display);
tab.parentNode.removeChild(tab);
var evt = document.createEvent("Events");
evt.initEvent("TabClose", true, false);
tab.dispatchEvent(evt);
]]></body>
</method>
<method name="createBrowser">
<parameter name="makeFront"/>
<parameter name="tab"/>
<parameter name="display"/>
<body><![CDATA[
const XUL_NS = "http://www.mozilla.org/keymaster/gatekeeper/there.is.only.xul";
var browser = document.createElementNS(XUL_NS, "browser");
browser.className = "deckbrowser-browser";
browser.setAttribute("style", "overflow: hidden");
browser.setAttribute("contextmenu", this.getAttribute("contextmenu"));
browser.setAttribute("autocompletepopup", this.getAttribute("autocompletepopup"));
browser.flex = 1;
if (makeFront)
browser.setAttribute("type", "content-primary");
else
browser.setAttribute("type", "content");
var nextDisplay;
var displayList = this.displayList;
if (tab) {
var nextTab = tab.nextSibling;
if (nextTab)
nextDisplay = this.getDisplayForTab(nextTab);
}
if (!display) {
display = document.createElementNS(XUL_NS, "deck");
displayList.insertBefore(display, nextDisplay);
}
display.appendChild(browser);
display.selectedIndex = 0;
if (this.progressListenerCreator) {
var listener = this.progressListenerCreator(this, browser);
browser.addProgressListener(listener);
display.progressListener = listener;
}
if (!tab) {
tab = document.createElementNS(XUL_NS, "richlistitem");
tab.setAttribute("type", "documenttab");
this.tabList.appendChild(tab);
}
if (tab == this.tabList.selectedItem) {
// already selected, but need to update the selected panel
display.lastAccess = Date.now();
displayList.selectedPanel = display;
}
else if (makeFront) {
this.tabList.selectedItem = tab;
}
return browser;
]]></body>
</method>
<method name="destroyBrowser">
<parameter name="browser"/>
<body><![CDATA[
if (!browser || browser == this.browser)
return;
var display = browser.parentNode;
this.saveBrowserState(display);
var domWin = browser.contentWindow;
var tab = this.getTabForDisplay(display);
if (tab)
tab.markInvalid();
const XHTML_NS = "http://www.w3.org/1999/xhtml";
var canvas = document.createElementNS(XHTML_NS, "canvas");
canvas.setAttribute("width", domWin.innerWidth);
canvas.setAttribute("height", domWin.innerHeight);
var ctx = canvas.getContext("2d");
ctx.drawWindow(domWin, 0, 0, domWin.innerWidth, domWin.innerHeight, "white");
display.insertBefore(canvas, display.firstChild);
display.lastAccess = Date.now();
display.progressListener = null;
display.removeChild(browser);
]]></body>
</method>
<method name="destroyEarliestBrowser">
<body><![CDATA[
var earliestBrowser = null;
var earliest = Date.now();
var displayList = this.displayList.childNodes;
for (var t = 0; t < displayList.length; t++) {
var display = displayList[t];
var browser = this.getBrowserForDisplay(display);
if (browser &&
display != this.displayList.selectedItem &&
display.lastAccess < earliest) {
earliestBrowser = browser;
earliest = display.lastAccess;
}
}
if (earliestBrowser)
this.destroyBrowser(earliestBrowser);
]]></body>
</method>
<method name="saveBrowserState">
<parameter name="display"/>
<body><![CDATA[
var state = { };
var browser = this.getBrowserForDisplay(display);
var doc = browser.contentDocument;
if (doc instanceof HTMLDocument) {
var tags = ["input", "textarea", "select"];
for (var t = 0; t < tags.length; t++) {
var elements = doc.getElementsByTagName(tags[t]);
for (var e = 0; e < elements.length; e++) {
var element = elements[e];
var id;
if (element.id)
id = "#" + element.id;
else if (element.name)
id = "$" + element.name;
if (id)
state[id] = element.value;
}
}
}
state._scrollX = browser.contentWindow.scrollX;
state._scrollY = browser.contentWindow.scrollY;
display.state = state;
]]></body>
</method>
<method name="restoreBrowserState">
<parameter name="display"/>
<body><![CDATA[
var state = display.state;
if (!state)
return;
var browser = this.getBrowserForDisplay(display);
var doc = browser.contentDocument;
for (item in state) {
var elem = null;
if (item.charAt(0) == "#") {
elem = doc.getElementById(item.substring(1));
}
else if (item.charAt(0) == "$") {
var list = doc.getElementsByName(item.substring(1));
if (list.length)
elem = list[0];
}
if (elem)
elem.value = state[item];
}
this.browser.contentWindow.scrollTo(state._scrollX, state._scrollY);
]]></body>
</method>
<method name="_browserToCanvas">
<body><![CDATA[
// FIXME: canvas needs to know it's actual width/height
// we should be able to use _canvas.width/_canvas.height here
// and can, most of the time
var rect = this._canvas.getBoundingClientRect();
var w = rect.right - rect.left;
var h = rect.bottom - rect.top;
this._canvas.width = w;
this._canvas.height = h;
var ctx = this._canvas.getContext("2d");
ctx.save();
ctx.scale(this._zoomLevel, this._zoomLevel);
ctx.drawWindow(this.browser.contentWindow,
this.dragData.pageX, this.dragData.pageY,
this._screenToPage(w), this._screenToPage(h),
"white");
ctx.restore();
]]></body>
</method>
<method name="_browserToCanvas2">
<parameter name="x"/>
<parameter name="y"/>
<parameter name="width"/>
<parameter name="height"/>
<body><![CDATA[
function intersect(r1, r2) {
let xmost1 = r1.x + r1.width;
let ymost1 = r1.y + r1.height;
let xmost2 = r2.x + r2.width;
let ymost2 = r2.y + r2.height;
let x = Math.max(r1.x, r2.x);
let y = Math.max(r1.y, r2.y);
let temp = Math.min(xmost1, xmost2);
if (temp <= x)
return null;
let width = temp - x;
temp = Math.min(ymost1, ymost2);
if (temp <= y)
return null;
let height = temp - y;
return {
x: x,
y: y,
width: width,
height: height
};
}
let r1 = { x : x,
y : y,
width : width,
height: height };
// check to see if the input coordinates are inside the visiable destination
let r2 = { x : this.dragData.pageX,
y : this.dragData.pageY,
width : this._canvas.width / this._zoomLevel,
height: this._canvas.height / this._zoomLevel };
let dest = intersect(r1, r2);
if (!dest)
return;
var ctx = this._canvas.getContext("2d");
ctx.save();
ctx.scale(this._zoomLevel, this._zoomLevel);
ctx.translate(dest.x - this.dragData.pageX, dest.y - this.dragData.pageY);
ctx.drawWindow(this.browser.contentWindow,
dest.x, dest.y,
dest.width, dest.height,
"white");
ctx.restore();
]]></body>
</method>
<method name="_updateCanvasPosition">
<body><![CDATA[
this._canvas.style.marginLeft = this.dragData.dragX + "px";
this._canvas.style.marginRight = -this.dragData.dragX + "px";
this._canvas.style.marginTop = this.dragData.dragY + "px";
this._canvas.style.marginBottom = -this.dragData.dragY + "px";
// Force a sync redraw
window.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindowUtils)
.processUpdates();
]]></body>
</method>
<method name="_clampZoomLevel">
<parameter name="aZoomLevel"/>
<body><![CDATA[
const min = 0.2;
const max = 2.0;
return Math.min(Math.max(min, aZoomLevel), max);
]]></body>
</method>
<property name="zoomLevel" onget="return this._zoomLevel;">
<setter><![CDATA[
this._zoomLevel = this._clampZoomLevel(val);
this._browserToCanvas();
return val;
]]></setter>
</property>
<method name="zoom">
<parameter name="aDirection"/>
<body><![CDATA[
if (aDirection == 0)
return;
var zoomDelta = 0.05; // 1/20
if (aDirection >= 0)
zoomDelta *= -1;
this._zoomLevel = this._clampZoomLevel(this._zoomLevel + zoomDelta);
this._browserToCanvas();
]]></body>
</method>
<method name="zoomToPage">
<body><![CDATA[
// Adjust the zoomLevel to fit the page contents in our window
// width
var [contentWidth, ] = this._contentAreaDimensions;
var canvasRect = this._canvas.getBoundingClientRect();
var canvasWidth = canvasRect.right - canvasRect.left;
if (contentWidth)
this._zoomLevel = canvasWidth / contentWidth;
]]></body>
</method>
<method name="zoomToElement">
<parameter name="aElement"/>
<body><![CDATA[
const margin = 15;
// scale to the element's width
var elRect = this._getPagePosition(aElement);
var zoomLevel = this.browser.boxObject.width / (elRect.width + (2 * margin));
this._zoomLevel = Math.min(zoomLevel, 10);
// pan to the element
this._panTo(Math.max(elRect.x - margin, 0),
Math.max(0, elRect.y - margin));
]]></body>
</method>
/**
* Retrieve the content element for a given point (relative to the top
* left corner of the browser window).
*/
<method name="elementFromPoint">
<parameter name="aX"/>
<parameter name="aY"/>
<body><![CDATA[
var cdoc = this.browser.contentDocument;
var [x, y] = this._clientToContentCoords(aX, aY);
var element = cdoc.elementFromPoint(x, y);
// Reset scroll state
this.browser.contentWindow.scrollTo(0, 0);
return element;
]]></body>
</method>
<method name="_getPagePosition">
<parameter name="aElement"/>
<body><![CDATA[
var r = aElement.getBoundingClientRect();
var retVal = {
width: r.right - r.left,
height: r.bottom - r.top,
x: r.left,
y: r.top
};
return retVal;
]]></body>
</method>
<method name="_redispatchMouseEvent">
<parameter name="aEvent"/>
<parameter name="aType"/>
<body><![CDATA[
if (!(aEvent instanceof MouseEvent)) {
Components.utils.reportError("_redispatchMouseEvent called with a non-mouse event");
return;
}
var [x, y] = this._clientToContentCoords(aEvent.clientX, aEvent.clientY);
var cwin = this.browser.contentWindow;
var cwu = cwin.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindowUtils);
cwu.sendMouseEvent(aType || aEvent.type,
x, y,
aEvent.button || 0,
aEvent.detail || 1,
0);
// Reset scroll state
cwin.scrollTo(0, 0);
]]></body>
</method>
<!-- Given a set of client coordinates (relative to the app window),
scrolls the content window as close to the clicked on content item
as possible, and returns the remaining offsets into the content
window if the object isn't at 0, 0. Callers should be sure to reset
scroll state after calling this method.
-->
<method name="_clientToContentCoords">
<parameter name="aClientX"/>
<parameter name="aClientY"/>
<body><![CDATA[
// Need to adjust for the toolbar height, etc.
var browserTop = this.browser.getBoundingClientRect().top;
var clickOffsetX = this._screenToPage(aClientX) + this.dragData.pageX;
var clickOffsetY = this._screenToPage(aClientY - browserTop) + this.dragData.pageY;
// Scroll the browser so that the event is targeted properly
var cwin = this.browser.contentWindow;
cwin.scrollTo(clickOffsetX, clickOffsetY);
// Might not have been able to scroll all the way if we're zoomed in,
// so we need to account for that difference.
var pageOffsetX = clickOffsetX - cwin.scrollX;
var pageOffsetY = clickOffsetY - cwin.scrollY;
return [pageOffsetX, pageOffsetY];
]]></body>
</method>
<property name="_contentAreaDimensions" readonly="true">
<getter>
var cdoc = this.browser.contentDocument;
// Return the document width/height for XUL documents (which is
// essentially the same as the viewport width/height).
if (cdoc instanceof XULDocument)
return [cdoc.width, cdoc.height];
// These might not exist yet depending on page load state
var body = cdoc.body || {};
var html = cdoc.documentElement || {};
var w = Math.max(body.scrollWidth, html.scrollWidth);
var h = Math.max(body.scrollHeight, html.scrollHeight);
if (isNaN(w) || isNaN(h))
return [this._canvas.width, this._canvas.height];
return [w, h];
</getter>
</property>
<property name="_effectiveCanvasDimensions" readonly="true">
<getter><![CDATA[
return [this._screenToPage(this._canvas.width),
this._screenToPage(this._canvas.height)];
]]></getter>
</property>
<property name="scrollX" readonly="true">
<getter><![CDATA[
return this.dragData.pageX - this._screenToPage(this.dragData.dragX);
]]></getter>
</property>
<property name="scrollY" readonly="true">
<getter><![CDATA[
return this.dragData.pageY - this._screenToPage(this.dragData.dragY);
]]></getter>
</property>
/**
* Given a set of page coordinates, constrain them such that they
* fit within the rect defined by [0,0] and [x,y], where x and y are
* the maximum values that can be used for the canvas' .top and .left
* such that it is still within the scrollable area of the page, taking
* into account the current zoomLevel.
*/
<method name="_constrainPanCoords">
<parameter name="aX"/>
<parameter name="aY"/>
<body><![CDATA[
var [contentAreaWidth, contentAreaHeight] = this._contentAreaDimensions;
var [canvasW, canvasH] = this._effectiveCanvasDimensions;
var offscreenWidth = contentAreaWidth - canvasW;
if (offscreenWidth <= 0) {
// Content is narrower than viewport, no need to pan horizontally
aX = 0;
} else {
// min of 0, max of contentAreaWidth - canvasW
var newPageX = Math.min(this.dragData.pageX + aX, offscreenWidth);
newPageX = Math.max(newPageX, 0);
aX = newPageX - this.dragData.pageX;
}
var offscreenHeight = contentAreaHeight - canvasH;
if (offscreenHeight <= 0) {
// Content is shorter than viewport, no need to pan vertically
aY = 0;
} else {
// min of 0, max of contentAreaHeight - canvasH
var newPageY = Math.min(this.dragData.pageY + aY, offscreenHeight);
newPageY = Math.max(newPageY, 0);
aY = newPageY - this.dragData.pageY;
}
return [aX, aY];
]]></body>
</method>
<method name="_screenToPage">
<parameter name="aValue"/>
<body><![CDATA[
return aValue / this._zoomLevel;
]]></body>
</method>
<method name="_pageToScreen">
<parameter name="aValue"/>
<body><![CDATA[
return aValue * this._zoomLevel;
]]></body>
</method>
<method name="_moveCanvas">
<parameter name="aDx"/>
<parameter name="aDy"/>
<body><![CDATA[
// Constrain offsets to the actual scrollWidth/scrollHeight
var [x, y] = this._constrainPanCoords(this._screenToPage(aDx), this._screenToPage(aDy));
// Canvas needs to move up for content to scroll down
this.dragData.dragX = -this._pageToScreen(x);
this.dragData.dragY = -this._pageToScreen(y);
this._updateCanvasPosition();
]]></body>
</method>
<method name="_startKinetic">
<body><![CDATA[
// Get the first and last mouse move event
let p2 = this._panEventTracker[this._panEventTrackerIndex];
let p1 = this._panEventTracker[(this._panEventTrackerIndex + 1) % this.PAN_EVENTS_TO_TRACK];
if (p2 && p1) {
let dx = p2.x - p1.x;
let dy = p2.y - p1.y;
let dt = p2.t - p1.t;
if (dt > 0) { // dt should never be less than 0
this.dragData.velocityX = dx / dt;
this.dragData.velocityY = dy / dt;
// Save the original x.y we're starting from to make sure
// we don't go backwards
this.dragData.originalX = this.dragData.dragX;
this.dragData.originalY = this.dragData.dragY;
// s = S0 + 0.5 * v0^2 * 1/CoK); s = position, s0 = initial pos
// v0 = initial velocity, CoK = Coefficient of Kinetic friction
// All in page coords
let idealDestScreenX = this.dragData.dragX + Math.abs(this.dragData.velocityX)
* this.dragData.velocityX * 200;
let idealDestScreenY = this.dragData.dragY + Math.abs(this.dragData.velocityY)
* this.dragData.velocityY * 200
let [destPageX, destPageY] = this._constrainPanCoords(-this._screenToPage(idealDestScreenX),
-this._screenToPage(idealDestScreenY));
// Convert to screen coords
this.dragData.destinationX = -this._pageToScreen(destPageX);
this.dragData.destinationY = -this._pageToScreen(destPageY);
// If we have a kinetic timer, kill it. This shouldn't happen
if (this.dragData.kineticId)
window.clearInterval(this.dragData.kineticId);
// Start timer for kinetic movements
let interval = dt / (this.PAN_EVENTS_TO_TRACK - 1);
this.dragData.kineticId = window.setInterval(this._doKinetic, interval, this, interval);
} else {
// dt <= 0, this is bad
this._endPan();
}
} else {
// p1 or p2 is null, either we didn't pan enough, or something went wrong
this._endPan()
}
// Clear out the old events since they aren't needed anymore
for (var i = 0; i < this.PAN_EVENTS_TO_TRACK; i++) {
this._panEventTracker[i] = null;
}
]]></body>
</method>
<method name="_doKinetic">
<parameter name="self"/>
<parameter name="dt"/>
<body><![CDATA[
// record where we're starting so we can test how far we went later
let startX = self.dragData.dragX;
let startY = self.dragData.dragY;
const stopTheshold = 3; //Distance from the destination point that we'll consider to be "there"
let dx = 0;
let dy = 0;
if (Math.abs(self.dragData.destinationX - self.dragData.dragX) < stopTheshold) {
dx = self.dragData.destinationX - self.dragData.dragX;
self.dragData.velocityX = dx/dt;
} else {
// decelerate, this assumes we decelerate perfectly to our destination
// it gets skewed if we're hitting an edge since we're using our progress
// instead of the time
dx = self.dragData.velocityX * dt * (Math.sqrt(Math.abs(self.dragData.destinationX - self.dragData.dragX))
/ Math.sqrt(Math.abs(self.dragData.destinationX - self.dragData.originalX)));
// if we're already at the destination, we don't want to move anymore
dx = self.dragData.originalX == self.dragData.destinationX ? 0 : dx;
}
if (Math.abs(self.dragData.destinationY - self.dragData.dragY) < stopTheshold) {
dx = self.dragData.destinationY - self.dragData.dragY;
self.dragData.velocityY = dx/dt;
} else {
// decelerate, this assumes we decelerate perfectly to our destination
// it gets skewed if we're hitting an edge since we're using our progress
// instead of the time
dy = self.dragData.velocityY * dt * (Math.sqrt(Math.abs(self.dragData.destinationY - self.dragData.dragY))
/ Math.sqrt(Math.abs(self.dragData.destinationY - self.dragData.originalY)));
// if we're already at the destination, we don't want to move anymore
dy = self.dragData.originalY == self.dragData.destinationY ? 0 : dy;
}
// Calculate the next x, y in screen space
let nextX = self.dragData.dragX + dx;
let nextY = self.dragData.dragY + dy;
// make sure we're still between original and destination coords
if((self.dragData.originalX > nextX &&
nextX > self.dragData.destinationX) ||
(self.dragData.originalX < nextX &&
nextX < self.dragData.destinationX))
self.dragData.dragX = nextX;
else
self.dragData.dragX = self.dragData.destinationX;
if((self.dragData.originalY > nextY &&
nextY > self.dragData.destinationY) ||
(self.dragData.originalY < nextY &&
nextY < self.dragData.destinationY))
self.dragData.dragY = nextY;
else
self.dragData.dragY = self.dragData.destinationY;
self._updateCanvasPosition();
// calculate how much we've actually moved and end if less than 4px
let actualDx = startX - self.dragData.dragX;
let actualDy = startY - self.dragData.dragY;
if ((actualDx / (self.dragData.destinationX - self.dragData.originalX) < 0 &&
actualDy / (self.dragData.destinationY - self.dragData.originalY) < 0) ||
(Math.abs(actualDx) < 4 && Math.abs(actualDy) < 4)) {
self._endKinetic();
}
]]></body>
</method>
<method name="_endKinetic">
<body><![CDATA[
window.clearInterval(this.dragData.kineticId);
this.dragData.kineticId = 0;
this._endPan();
]]></body>
</method>
<!-- Pans directly to a given X/Y (in page coordinates) -->
<method name="_panTo">
<parameter name="aX"/>
<parameter name="aY"/>
<body><![CDATA[
var [deltaX, deltaY] = this._constrainPanCoords(aX - this.dragData.pageX,
aY - this.dragData.pageY);
this.dragData.pageX += deltaX;
this.dragData.pageY += deltaY;
this._browserToCanvas();
]]></body>
</method>
<method name="_dragStartTimer">
<body><![CDATA[
this.dragData.dragging = true;
this._dragStartTimeout = -1;
]]></body>
</method>
<method name="_endPan">
<body><![CDATA[
// dragX/dragY are guaranteed to be within the correct bounds, so just
// update pageX/pageY directly.
this.dragData.pageX -= this._screenToPage(this.dragData.dragX);
this.dragData.pageY -= this._screenToPage(this.dragData.dragY);
// relocate the canvas to 0x0 in the window
this.dragData.dragX = 0;
this.dragData.dragY = 0;
// update canvas position and draw the canvas at the new location
this._browserToCanvas();
this._updateCanvasPosition();
]]></body>
</method>
<field name="stackEventHandler">
<![CDATA[
({
deckbrowser: this,
handleEvent: function seh_handleEvent(aEvent) {
if (!(aEvent.type in this)) {
Components.reportError("MouseController called with unknown event type " + aEvent.type + "\n");
return;
}
this[aEvent.type](aEvent);
},
mousedown: function seh_mousedown(aEvent) {
if (aEvent.button != 0)
return;
// stop kinetic scrolling if it's in progress
// avoid setting _lastMouseDown in that case so that we don't
// redispatch it in mouseup
let dragData = this.deckbrowser.dragData;
if (dragData.kineticId) {
this.deckbrowser._endKinetic();
} else {
// Keep a reference to the event so that we can redispatch it
// on mouseup
this._lastMouseDown = aEvent;
}
// kinetic gets canceled above, so we should be guaranteed to not be
// in the panning state
if (dragData.dragging)
throw "Mousedown while panning - how'd this happen?";
// The start of the current portion drag
dragData.sX = aEvent.screenX;
dragData.sY = aEvent.screenY;
// The total delta between current mouse position and sX/sY
dragData.dragX = 0;
dragData.dragY = 0;
this.deckbrowser._dragStartTimeout = setTimeout(function (db) {
db._dragStartTimer();
}, 200, this.deckbrowser);
},
mouseup: function seh_mouseup(aEvent) {
if (aEvent.button != 0)
return;
// cancel scrollStart timer if it's pending
clearTimeout(this.deckbrowser._dragStartTimeout);
this.deckbrowser._dragStartTimeout = -1;
// If we're panning, stop dragging and start kinetic scrolling
if (this.deckbrowser.dragData.dragging) {
this.deckbrowser.dragData.dragging = false;
if (this.deckbrowser._allowKinetic)
this.deckbrowser._startKinetic();
else
this.deckbrowser._endPan();
return;
}
// Otherwise, dispatch the click event (if this mouseup was on the canvas)
if (this._lastMouseDown &&
aEvent.originalTarget == this.deckbrowser._canvas) {
// send mousedown & mouseup
this.deckbrowser._redispatchMouseEvent(this._lastMouseDown);
this._lastMouseDown = null;
this.deckbrowser._redispatchMouseEvent(aEvent);
// FIXME: dblclick events don't fire on the n810, check to see if
// we should treat this as a double-click
if (this._lastMouseUp &&
(aEvent.timeStamp - this._lastMouseUp.timeStamp) < 400 &&
Math.abs(aEvent.clientX - this._lastMouseUp.clientX) < 30 &&
Math.abs(aEvent.clientY - this._lastMouseUp.clientY) < 30) {
this.dblclick(aEvent);
return;
}
this._lastMouseUp = aEvent;
}
},
mousemove: function seh_mousemove(aEvent) {
if (!this.deckbrowser.dragData.dragging) {
// If we've moved more than N pixels lets go ahead and assume we're dragging
// and not wait for the timeout to complete.
if (this.deckbrowser._dragStartTimeout != -1 &&
(Math.abs(this.deckbrowser.dragData.sX - aEvent.screenX) > 10 ||
Math.abs(this.deckbrowser.dragData.sY - aEvent.screenY) > 10)) {
clearTimeout(this.deckbrowser._dragStartTimeout);
this.deckbrowser._dragStartTimer();
} else {
return false;
}
}
var dx = this.deckbrowser.dragData.sX - aEvent.screenX;
var dy = this.deckbrowser.dragData.sY - aEvent.screenY;
// Filter out noise in big panning operations which are
// almost certainly intended to be on-axis horizontal or
// vertical pans.
if (Math.abs(dx) > 40 || Math.abs(dy) > 40) {
if (Math.abs(dx/dy) < 0.3) // dx is a lot less than dy, probably a vertical drag
dx = 0;
else if (Math.abs(dy/dx) < 0.3) // probably a horizontal drag
dy = 0;
}
let now = Date.now();
this.deckbrowser._panEventTrackerIndex = (this.deckbrowser._panEventTrackerIndex + 1) % this.deckbrowser.PAN_EVENTS_TO_TRACK;
var pt = {
x: aEvent.screenX,
y: aEvent.screenY,
t: now
};
this.deckbrowser._panEventTracker[this.deckbrowser._panEventTrackerIndex] = pt;
this.deckbrowser._moveCanvas(dx, dy);
aEvent.preventDefault();
return true;
},
DOMMouseScroll: function seh_DOMMouseScroll(aEvent) {
this.deckbrowser.zoom(aEvent.detail);
},
dblclick: function seh_dblclick(aEvent) {
var target = aEvent.originalTarget;
var dragData = this.deckbrowser.dragData;
if (this.deckbrowser._zoomed) {
// reset zoom, pan state
this.deckbrowser._zoomLevel = this._oldZoomLevel;
[dragData.pageX, dragData.pageY] = [dragData.oldPageX, dragData.oldPageY];
this.deckbrowser._browserToCanvas();
this.deckbrowser._zoomed = false;
} else {
var element = this.deckbrowser.elementFromPoint(aEvent.clientX, aEvent.clientY);
if (!element) {
Components.utils.reportError("elementFromPoint returned null\n");
return;
}
// Find the nearest non-inline ancestor
while (element.parentNode) {
var display = window.getComputedStyle(element, "").getPropertyValue("display");
var zoomable = /table/.test(display) || /block/.test(display);
if (zoomable)
break;
element = element.parentNode;
}
// Remember pageX/pageY
[dragData.oldPageX, dragData.oldPageY] = [dragData.pageX, dragData.pageY];
this._oldZoomLevel = this.deckbrowser._zoomLevel;
this.deckbrowser.zoomToElement(element);
this.deckbrowser._zoomed = true;
}
}
});
]]>
</field>
</implementation>
</binding>
<binding id="documenttab"
extends="chrome://global/content/bindings/richlistbox.xml#richlistitem">
<content>
<xul:stack anonid="page" class="documenttab-container" flex="1">
<html:canvas anonid="canvas" class="documenttab-canvas" width="80" height="60"/>
<xul:vbox align="start">
<xul:image anonid="close" class="documenttab-close"/>
</xul:vbox>
</xul:stack>
</content>
<implementation>
<method name="updateTab">
<parameter name="browser"/>
<body>
<![CDATA[
let canvas = document.getAnonymousElementByAttribute(this, "anonid", "canvas");
let domWin = browser.contentWindow;
let width = domWin.innerWidth;
let height = domWin.innerHeight;
let ctx = canvas.getContext("2d");
ctx.clearRect(0, 0, 80, 60);
ctx.restore(); // XXXndeakin remove this
ctx.save();
ctx.scale(80 / width, 60 / height);
ctx.drawWindow(domWin, 0, 0, width, height, "white");
ctx.restore();
]]>
</body>
</method>
<method name="markInvalid">
<parameter name="browser"/>
<body>
<![CDATA[
let canvas = document.getAnonymousElementByAttribute(this, "anonid", "canvas");
let ctx = canvas.getContext("2d");
ctx.save();
ctx.strokeStyle = "red";
ctx.moveTo(63, 43);
ctx.lineTo(78, 58);
ctx.moveTo(78, 43);
ctx.lineTo(63, 58);
ctx.stroke();
ctx.restore();
]]>
</body>
</method>
</implementation>
</binding>
</bindings>