gecko/mobile/chrome/content/deckbrowser.xml

1516 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(this.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, this._canvas);
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 visible destination
let [canvasW, canvasH] = this._effectiveCanvasDimensions;
let r2 = {
x : this.dragData.pageX,
y : this.dragData.pageY,
width : canvasW,
height: canvasH
};
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 in client coordinates
- (relative to the top left corner of the chrome window).
-->
<method name="elementFromPoint">
<parameter name="aX"/>
<parameter name="aY"/>
<body><![CDATA[
var [x, y] = this._clientToContentCoords(aX, aY);
var cwu = this.browser.contentWindow
.QueryInterface(Components.interfaces.nsIInterfaceRequestor)
.getInterface(Components.interfaces.nsIDOMWindowUtils);
var element = cwu.elementFromPoint(x, y,
true, /* ignore root scroll frame */
false); /* don't flush layout*/
return element;
]]></body>
</method>
<!--
- Retrieve the page position for a given element
- (relative to the document origin).
-->
<method name="_getPagePosition">
<parameter name="aElement"/>
<body><![CDATA[
var r = aElement.getBoundingClientRect();
var cwin = this.browser.contentWindow;
var retVal = {
width: r.width,
height: r.height,
x: r.left + cwin.scrollX,
y: r.top + cwin.scrollY
};
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);
// Redispatch the mouse event, ignoring the root scroll frame
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, true);
]]></body>
</method>
<!-- Given a set of client coordinates (relative to the app window),
returns the content coordinates relative to the viewport.
-->
<method name="_clientToContentCoords">
<parameter name="aClientX"/>
<parameter name="aClientY"/>
<body><![CDATA[
// Determine position relative to the document origin
// Need to adjust for the deckbrowser not being at 0,0
// (e.g. due to other browser UI)
let browserRect = this.getBoundingClientRect();
let clickOffsetX = this._screenToPage(aClientX - browserRect.left) + this.dragData.pageX;
let clickOffsetY = this._screenToPage(aClientY - browserRect.top) + this.dragData.pageY;
// Take scroll offset into account to return coordinates relative to the viewport
let cwin = this.browser.contentWindow;
return [clickOffsetX - cwin.scrollX,
clickOffsetY - cwin.scrollY];
]]></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"/>
<parameter name="srcCanvas"/>
<body>
<![CDATA[
const tabWidth = 80
const tabHeight = 60
let canvas = document.getAnonymousElementByAttribute(this, "anonid", "canvas");
let domWin = browser.contentWindow;
let ctx = canvas.getContext("2d");
if (srcCanvas) {
let width = tabWidth * srcCanvas.width / domWin.innerWidth;
let height = tabHeight * srcCanvas.height / domWin.innerHeight;
ctx.drawImage(srcCanvas, 0, 0, width, height)
} else {
let width = domWin.innerWidth;
let height = domWin.innerHeight;
ctx.clearRect(0, 0, tabWidth, tabHeight);
ctx.save();
ctx.scale(tabWidth / width, tabHeight / 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>