gecko/mobile/chrome/content/deckbrowser.xml

1507 lines
52 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 flex="1">
<html:div anonid="viewport" style="-moz-stack-sizing: ignore; overflow: hidden; background-image:url('chrome://browser/content/checkerboard.png')">
<html:canvas anonid="ccanvas"
moz-opaque="true"
style="height: 200%; width: 200%;"/>
</html:div>
</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;
var prefsvc = Components.classes["@mozilla.org/preferences-service;1"].
getService(Components.interfaces.nsIPrefBranch2);
this._allowKinetic = prefsvc.getBoolPref("browser.ui.panning.kinetic");
// panning
this._viewport.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._viewport.addEventListener("mousemove", this.stackEventHandler, true);
// zoom
// FIXME: dblclicks don't work on the device
// this._viewport.addEventListener("dblclick", this.stackEventHandler, true);
this._viewport.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="viewportDimensions" readonly="true">
<getter><![CDATA[
var rect = this._viewport.getBoundingClientRect();
return [rect.width, rect.height];
]]></getter>
</property>
<property name="_effectiveViewportDimensions" readonly="true">
<getter><![CDATA[
var [w, h] = this.viewportDimensions;
return [this._screenToPage(w), this._screenToPage(h)];
]]></getter>
</property>
<property name="_effectiveCanvasDimensions" readonly="true">
<getter><![CDATA[
let canvasRect = this._canvas.getBoundingClientRect();
return [this._screenToPage(canvasRect.width),
this._screenToPage(canvasRect.height)];
]]></getter>
</property>
<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="_viewport">
document.getAnonymousElementByAttribute(this, "anonid", "viewport");
</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="browsers" readonly="true">
<getter>
<![CDATA[
var browsers = [];
var tabList = this.tabList.children;
for (var t = 0; t < tabList.length; t++)
browsers.push(getBrowserForDisplay(this.displayList.childNodes[t]));
return browsers;
]]>
</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[
// Clear the whole canvas
// we clear the whole canvas because the browser's width or height
// could be less than the area we end up actually drawing.
// XXX we could be smarter about this and only clear when necessary
var ctx = this._canvas.getContext("2d");
ctx.clearRect(0, 0, this._canvas.width, this._canvas.height);
// Reset 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.children;
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.getItemAtIndex(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="isLoading">
<parameter name="browser"/>
<body><![CDATA[
return browser.parentNode.loading || false;
]]></body>
</method>
<method name="setLoading">
<parameter name="browser"/>
<parameter name="state"/>
<body><![CDATA[
browser.parentNode.loading = state;
if (state) {
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;
let cwin = self.browser.contentWindow;
for (let i = 0; i < aEvent.clientRects.length; i++) {
let e = aEvent.clientRects.item(i);
//dump(Math.floor(e.left + cwin.scrollX),
// Math.floor(e.top + cwin.scrollY),
// Math.ceil(e.width), Math.ceil(e.height));
self._redrawRect(Math.floor(e.left + cwin.scrollX),
Math.floor(e.top + cwin.scrollY),
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 tabList = this.tabList;
var nextTab = tabList.selectedItem;
var tabidx = tabList.getIndexOfItem(tab);
if (tab == tabList.selectedItem) {
nextTab = tabList.getItemAtIndex(tabidx + 1) || tabList.getItemAtIndex(tabidx - 1);
if (!nextTab)
return;
}
var display = this.getDisplayForTab(tab);
if (display)
display.parentNode.removeChild(display);
tabList.removeTab(tab);
// redraw the tabs
for (var t = tabidx; t < this.tabList.itemCount; t++) {
var tab = tabList.getItemAtIndex(t);
var browser = this.getBrowserForDisplay(this.getDisplayForTab(tab));
if (browser)
tab.updateTab(browser);
}
this.tabList.selectedItem = nextTab;
this.displayList.selectedPanel = this.getDisplayForTab(nextTab);
this.selectTab(nextTab);
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);
}
var uniqueId = Date.now() + displayList.childNodes.length;
if (!display) {
display = document.createElementNS(XUL_NS, "deck");
display.setAttribute("id", "display-" + uniqueId)
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("id", "tab-" + uniqueId)
tab.setAttribute("type", "documenttab");
this.tabList.addTab(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>
<property name="_canvasPageOffset" readonly="true">
<getter><![CDATA[
let [canvasW, canvasH] = this._effectiveCanvasDimensions;
let [viewportW, viewportH] = this._effectiveViewportDimensions;
let offscreenCanvasW = (canvasW - viewportW);
let offscreenCanvasH = (canvasH - viewportH);
let [contentWidth, contentHeight] = this._contentAreaDimensions;
let left = Math.max(-this.dragData.pageX, -(offscreenCanvasW / 2));
let rightMost = (contentWidth - canvasW);
if (left > rightMost && rightMost > 0)
left = rightMost;
let top = Math.max(-this.dragData.pageY, -(offscreenCanvasH / 2));
let bottomMost = (contentHeight - canvasH);
if (top > bottomMost && bottomMost > 0)
top = bottomMost;
return [left, top];
]]></getter>
</property>
<property name="_drawOffset" readonly="true">
<getter><![CDATA[
let [offX, offY] = this._canvasPageOffset;
return [this.dragData.pageX + offX, this.dragData.pageY + offY];
]]></getter>
</property>
<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();
this._canvas.width = rect.width;
this._canvas.height = rect.height;
var [x, y] = this._drawOffset;
var ctx = this._canvas.getContext("2d");
ctx.save();
ctx.scale(this._zoomLevel, this._zoomLevel);
ctx.drawWindow(this.browser.contentWindow,
x, y,
this._screenToPage(rect.width),
this._screenToPage(rect.height),
"white",
ctx.DRAWWINDOW_DO_NOT_FLUSH);
ctx.restore();
this._updateCanvasPosition();
]]></body>
</method>
<method name="_redrawRect">
<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);
var [offX, offY] = this._drawOffset;
ctx.translate(dest.x - offX,
dest.y - offY);
ctx.drawWindow(this.browser.contentWindow,
dest.x, dest.y,
dest.width, dest.height,
"white",
ctx.DRAWWINDOW_DO_NOT_FLUSH);
ctx.restore();
]]></body>
</method>
<method name="_updateCheckerboard">
<body><![CDATA[
let x = Math.round(this.dragData.dragX);
let y = Math.round(this.dragData.dragY);
this._viewport.style.backgroundPosition = x+"px "+ y+"px";
]]></body>
</method>
<method name="_updateCanvasPosition">
<body><![CDATA[
let x = Math.round(this.dragData.dragX);
let y = Math.round(this.dragData.dragY);
let [offX, offY] = this._canvasPageOffset;
[x, y] = [this._pageToScreen(offX) + x, this._pageToScreen(offY) + y];
this._canvas.style.marginLeft = x + "px";
this._canvas.style.marginRight = -x + "px";
this._canvas.style.marginTop = y + "px";
this._canvas.style.marginBottom = -y + "px";
this._updateCheckerboard();
// 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 [viewportW, ] = this.viewportDimensions;
this._zoomLevel = viewportW / contentWidth;
this._browserToCanvas();
]]></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);
return element;
]]></body>
</method>
<method name="_getPagePosition">
<parameter name="aElement"/>
<body><![CDATA[
var r = aElement.getBoundingClientRect();
var retVal = {
width: r.width,
height: r.height,
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);
]]></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 deckbrowser not being at 0,0
// (e.g. due to other browser UI)
var browserRect = this.getBoundingClientRect();
var clickOffsetX = this._screenToPage(aClientX - browserRect.left) + this.dragData.pageX;
var clickOffsetY = this._screenToPage(aClientY - browserRect.top) + 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="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 [viewportW, viewportH] = this._effectiveViewportDimensions;
var offscreenWidth = contentAreaWidth - viewportW;
if (offscreenWidth <= 0) {
// Content is narrower than viewport, no need to pan horizontally
aX = 0;
} else {
// min of 0, max of contentAreaWidth - viewportW
var newPageX = Math.min(this.dragData.pageX + aX, offscreenWidth);
newPageX = Math.max(newPageX, 0);
aX = newPageX - this.dragData.pageX;
}
var offscreenHeight = contentAreaHeight - viewportH;
if (offscreenHeight <= 0) {
// Content is shorter than viewport, no need to pan vertically
aY = 0;
} else {
// min of 0, max of contentAreaHeight - viewportH
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>
<!-- ensures that a given content element is visible -->
<method name="ensureElementIsVisible">
<parameter name="aElement"/>
<body><![CDATA[
let elRect = this._getPagePosition(aElement);
let [viewportW, viewportH] = this._effectiveViewportDimensions;
let curRect = {
x: this.dragData.pageX,
y: this.dragData.pageY,
width: viewportW,
height: viewportH
}
// Adjust for part of our viewport being offscreen
// XXX this assumes that the browser is meant to be fullscreen
let browserRect = this.getBoundingClientRect();
curRect.height -= this._screenToPage(Math.abs(browserRect.top));
if (browserRect.top < 0)
curRect.y -= this._screenToPage(browserRect.top);
curRect.width -= this._screenToPage(Math.abs(browserRect.left));
if (browserRect.left < 0)
curRect.x -= this._screenToPage(browserRect.left);
let newx = curRect.x;
let newy = curRect.y;
if (elRect.x + elRect.width > curRect.x + curRect.width) {
newx = curRect.x + ((elRect.x + elRect.width)-(curRect.x + curRect.width));
} else if (elRect.x < curRect.x) {
newx = elRect.x;
}
if (elRect.y + elRect.height > curRect.y + curRect.height) {
newy = curRect.y + ((elRect.y + elRect.height)-(curRect.y + curRect.height));
} else if (elRect.y < curRect.y) {
newy = elRect.y;
}
this.panTo(newx, newy);
]]></body>
</method>
<!-- Pans directly to a given content element -->
<method name="panToElement">
<parameter name="aElement"/>
<body><![CDATA[
var elRect = this._getPagePosition(aElement);
this.panTo(elRect.x, elRect.y);
]]></body>
</method>
<!-- Pans directly to a given X/Y (in content coordinates) -->
<method name="panTo">
<parameter name="aX"/>
<parameter name="aY"/>
<body><![CDATA[
this.panBy(aX - this.dragData.pageX, aY - this.dragData.pageY);
]]></body>
</method>
<!-- Pans X/Y pixels (in content coordinates) -->
<method name="panBy">
<parameter name="dX"/>
<parameter name="dY"/>
<body><![CDATA[
if (dX == 0 && dY == 0)
return;
var [deltaX, deltaY] = this._constrainPanCoords(dX, dY);
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);
// reset the drag data
this.dragData.dragX = 0;
this.dragData.dragY = 0;
// update the canvas and move it to the right position
this._browserToCanvas();
]]></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>
<constructor><![CDATA[
let close = document.getAnonymousElementByAttribute(this, "anonid", "close");
let closefn = new Function("event", this.control.getAttribute("onclosetab"));
var self = this;
close.addEventListener("mousedown", function(event) { closefn.call(self, event); event.stopPropagation(); }, true);
]]></constructor>
<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>
<!-- very hacky, used to display richlistitems in multiple columns -->
<binding id="tablist"
extends="chrome://global/content/bindings/richlistbox.xml#richlistbox">
<content>
<children includes="listheader"/>
<xul:scrollbox allowevents="true" orient="horizontal" anonid="main-box"
flex="1" style="overflow: auto;">
<children/>
</xul:scrollbox>
</content>
<implementation>
<field name="tabsPerColumn">4</field>
<property name="children" readonly="true">
<getter>
<![CDATA[
var childNodes = [];
for (var box = this.firstChild; box; box = box.nextSibling) {
for (var child = box.firstChild; child; child = child.nextSibling) {
if (child instanceof Components.interfaces.nsIDOMXULSelectControlItemElement)
childNodes.push(child);
}
}
return childNodes;
]]>
</getter>
</property>
<method name="addTab">
<parameter name="tab"/>
<body>
<![CDATA[
if (this.children.length % this.tabsPerColumn == 0)
this.appendChild(document.createElement("vbox"));
this.lastChild.appendChild(tab);
return tab;
]]>
</body>
</method>
<method name="removeTab">
<parameter name="tab"/>
<body>
<![CDATA[
var idx = this.getIndexOfItem(tab);
if (idx == -1)
return;
// remove all later tabs and readd them so that there aren't empty columns
var count = this.itemCount - 1;
var tomove = [ ];
for (var c = count; c >= idx; c--) {
var tab = this.getItemAtIndex(c);
tomove.push(tab.parentNode.removeChild(tab));
if (!this.lastChild.hasChildNodes())
this.removeChild(this.lastChild);
}
// subtract 2 because the tab to remove should not be added back again
for (var m = tomove.length - 2; m >= 0; m--)
this.addTab(tomove[m]);
]]>
</body>
</method>
</implementation>
</binding>
</bindings>