/toolkit/content/tests/fennec-tile-testapp/chrome/content/WidgetStack.js
http://github.com/zpao/v8monkey · JavaScript · 1465 lines · 913 code · 281 blank · 271 comment · 258 complexity · f4a3ba8e0be58bf4b9725f1c1f0f8b38 MD5 · raw file
- /* -*- Mode: js2; tab-width: 40; indent-tabs-mode: nil; js2-basic-offset: 2; js2-skip-preprocessor-directives: t; -*- */
- /*
- * ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Mozilla Mobile Browser.
- *
- * The Initial Developer of the Original Code is
- * Mozilla Corporation.
- * Portions created by the Initial Developer are Copyright (C) 2008
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- * Vladimir Vukicevic <vladimir@pobox.com>
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
- var gWsDoLog = false;
- var gWsLogDiv = null;
- function logbase() {
- if (!gWsDoLog)
- return;
- if (gWsLogDiv == null && "console" in window) {
- console.log.apply(console, arguments);
- } else {
- var s = "";
- for (var i = 0; i < arguments.length; i++) {
- s += arguments[i] + " ";
- }
- s += "\n";
- if (gWsLogDiv) {
- gWsLogDiv.appendChild(document.createElementNS("http://www.w3.org/1999/xhtml", "br"));
- gWsLogDiv.appendChild(document.createTextNode(s));
- }
- dump(s);
- }
- }
- function dumpJSStack(stopAtNamedFunction) {
- let caller = Components.stack.caller;
- dump("\tStack: " + caller.name);
- while ((caller = caller.caller)) {
- dump(" <- " + caller.name);
- if (stopAtNamedFunction && caller.name != "anonymous")
- break;
- }
- dump("\n");
- }
- function log() {
- return;
- logbase.apply(window, arguments);
- }
- function log2() {
- return;
- logbase.apply(window, arguments);
- }
- let reportError = log;
- /*
- * wsBorder class
- *
- * Simple container for top,left,bottom,right "border" values
- */
- function wsBorder(t, l, b, r) {
- this.setBorder(t, l, b, r);
- }
- wsBorder.prototype = {
- setBorder: function (t, l, b, r) {
- this.top = t;
- this.left = l;
- this.bottom = b;
- this.right = r;
- },
- toString: function () {
- return "[l:" + this.left + ",t:" + this.top + ",r:" + this.right + ",b:" + this.bottom + "]";
- }
- };
- /*
- * wsRect class
- *
- * Rectangle class, with both x/y/w/h and t/l/b/r accessors.
- */
- function wsRect(x, y, w, h) {
- this.left = x;
- this.top = y;
- this.right = x+w;
- this.bottom = y+h;
- }
- wsRect.prototype = {
- get x() { return this.left; },
- get y() { return this.top; },
- get width() { return this.right - this.left; },
- get height() { return this.bottom - this.top; },
- set x(v) {
- let diff = this.left - v;
- this.left = v;
- this.right -= diff;
- },
- set y(v) {
- let diff = this.top - v;
- this.top = v;
- this.bottom -= diff;
- },
- set width(v) { this.right = this.left + v; },
- set height(v) { this.bottom = this.top + v; },
- setRect: function(x, y, w, h) {
- this.left = x;
- this.top = y;
- this.right = x+w;
- this.bottom = y+h;
- return this;
- },
- setBounds: function(t, l, b, r) {
- this.top = t;
- this.left = l;
- this.bottom = b;
- this.right = r;
- return this;
- },
- equals: function equals(r) {
- return (r != null &&
- this.top == r.top &&
- this.left == r.left &&
- this.bottom == r.bottom &&
- this.right == r.right);
- },
- clone: function clone() {
- return new wsRect(this.left, this.top, this.right - this.left, this.bottom - this.top);
- },
- center: function center() {
- return [this.left + (this.right - this.left) / 2,
- this.top + (this.bottom - this.top) / 2];
- },
- centerRounded: function centerRounded() {
- return this.center().map(Math.round);
- },
- copyFrom: function(r) {
- this.top = r.top;
- this.left = r.left;
- this.bottom = r.bottom;
- this.right = r.right;
- return this;
- },
- copyFromTLBR: function(r) {
- this.left = r.left;
- this.top = r.top;
- this.right = r.right;
- this.bottom = r.bottom;
- return this;
- },
- translate: function(x, y) {
- this.left += x;
- this.right += x;
- this.top += y;
- this.bottom += y;
- return this;
- },
- // return a new wsRect that is the union of that one and this one
- union: function(rect) {
- let l = Math.min(this.left, rect.left);
- let r = Math.max(this.right, rect.right);
- let t = Math.min(this.top, rect.top);
- let b = Math.max(this.bottom, rect.bottom);
- return new wsRect(l, t, r-l, b-t);
- },
- toString: function() {
- return "[" + this.x + "," + this.y + "," + this.width + "," + this.height + "]";
- },
- expandBy: function(b) {
- this.left += b.left;
- this.right += b.right;
- this.top += b.top;
- this.bottom += b.bottom;
- return this;
- },
- contains: function(other) {
- return !!(other.left >= this.left &&
- other.right <= this.right &&
- other.top >= this.top &&
- other.bottom <= this.bottom);
- },
- intersect: function(r2) {
- let xmost1 = this.right;
- let xmost2 = r2.right;
- let x = Math.max(this.left, r2.left);
- let temp = Math.min(xmost1, xmost2);
- if (temp <= x)
- return null;
- let width = temp - x;
- let ymost1 = this.bottom;
- let ymost2 = r2.bottom;
- let y = Math.max(this.top, r2.top);
- temp = Math.min(ymost1, ymost2);
- if (temp <= y)
- return null;
- let height = temp - y;
- return new wsRect(x, y, width, height);
- },
- intersects: function(other) {
- let xok = (other.left > this.left && other.left < this.right) ||
- (other.right > this.left && other.right < this.right) ||
- (other.left <= this.left && other.right >= this.right);
- let yok = (other.top > this.top && other.top < this.bottom) ||
- (other.bottom > this.top && other.bottom < this.bottom) ||
- (other.top <= this.top && other.bottom >= this.bottom);
- return xok && yok;
- },
- /**
- * Similar to (and most code stolen from) intersect(). A restriction
- * is an intersection, but this modifies the receiving object instead
- * of returning a new rect.
- */
- restrictTo: function restrictTo(r2) {
- let xmost1 = this.right;
- let xmost2 = r2.right;
- let x = Math.max(this.left, r2.left);
- let temp = Math.min(xmost1, xmost2);
- if (temp <= x)
- throw "Intersection is empty but rects cannot be empty";
- let width = temp - x;
- let ymost1 = this.bottom;
- let ymost2 = r2.bottom;
- let y = Math.max(this.top, r2.top);
- temp = Math.min(ymost1, ymost2);
- if (temp <= y)
- throw "Intersection is empty but rects cannot be empty";
- let height = temp - y;
- return this.setRect(x, y, width, height);
- },
- /**
- * Similar to (and most code stolen from) union(). An extension is a
- * union (in our sense of the term, not the common set-theoretic sense),
- * but this modifies the receiving object instead of returning a new rect.
- * Effectively, this rectangle is expanded minimally to contain all of the
- * other rect. "Expanded minimally" means that the rect may shrink if
- * given a strict subset rect as the argument.
- */
- expandToContain: function extendTo(rect) {
- let l = Math.min(this.left, rect.left);
- let r = Math.max(this.right, rect.right);
- let t = Math.min(this.top, rect.top);
- let b = Math.max(this.bottom, rect.bottom);
- return this.setRect(l, t, r-l, b-t);
- },
- round: function round(scale) {
- if (!scale) scale = 1;
- this.left = Math.floor(this.left * scale) / scale;
- this.top = Math.floor(this.top * scale) / scale;
- this.right = Math.ceil(this.right * scale) / scale;
- this.bottom = Math.ceil(this.bottom * scale) / scale;
- return this;
- },
- scale: function scale(xscl, yscl) {
- this.left *= xscl;
- this.right *= xscl;
- this.top *= yscl;
- this.bottom *= yscl;
- return this;
- }
- };
- /*
- * The "Widget Stack"
- *
- * Manages a <xul:stack>'s children, allowing them to be dragged around
- * the stack, subject to specified constraints. Optionally supports
- * one widget designated as the viewport, which can be panned over a virtual
- * area without needing to draw that area entirely. The viewport widget
- * is designated by a 'viewport' attribute on the child element.
- *
- * Widgets are subject to various constraints, specified in xul via the
- * 'constraint' attribute. Current constraints are:
- * ignore-x: When panning, ignore any changes to the widget's x position
- * ignore-y: When panning, ignore any changes to the widget's y position
- * vp-relative: This widget's position should be claculated relative to
- * the viewport widget. It will always keep the same offset from that
- * widget as initially laid out, regardless of changes to the viewport
- * bounds.
- * frozen: This widget is in a fixed position and should never pan.
- */
- function WidgetStack(el, ew, eh) {
- this.init(el, ew, eh);
- }
- WidgetStack.prototype = {
- // the <stack> element
- _el: null,
- // object indexed by widget id, with state struct for each object (see _addNewWidget)
- _widgetState: null,
- // any barriers
- _barriers: null,
- // If a viewport widget is present, this will point to its state object;
- // otherwise null.
- _viewport: null,
- // a wsRect; the inner bounds of the viewport content
- _viewportBounds: null,
- // a wsBorder; the overflow area to the side of the bounds where our
- // viewport-relative widgets go
- _viewportOverflow: null,
- // a wsRect; the viewportBounds expanded by the viewportOverflow
- _pannableBounds: null,
- get pannableBounds() {
- if (!this._pannableBounds) {
- this._pannableBounds = this._viewportBounds.clone()
- .expandBy(this._viewportOverflow);
- }
- return this._pannableBounds.clone();
- },
- // a wsRect; the currently visible part of pannableBounds.
- _viewingRect: null,
- // the amount of current global offset applied to all widgets (whether
- // static or not). Set via offsetAll(). Can be used to push things
- // out of the way for overlaying some other UI.
- globalOffsetX: 0,
- globalOffsetY: 0,
- // if true (default), panning is constrained to the pannable bounds.
- _constrainToViewport: true,
- _viewportUpdateInterval: -1,
- _viewportUpdateTimeout: -1,
- _viewportUpdateHandler: null,
- _panHandler: null,
- _dragState: null,
- _skipViewportUpdates: 0,
- _forceViewportUpdate: false,
- //
- // init:
- // el: the <stack> element whose children are to be managed
- //
- init: function (el, ew, eh) {
- this._el = el;
- this._widgetState = {};
- this._barriers = [];
- let rect = this._el.getBoundingClientRect();
- let width = rect.width;
- let height = rect.height;
- if (ew != undefined && eh != undefined) {
- width = ew;
- height = eh;
- }
- this._viewportOverflow = new wsBorder(0, 0, 0, 0);
- this._viewingRect = new wsRect(0, 0, width, height);
- // listen for DOMNodeInserted/DOMNodeRemoved/DOMAttrModified
- let children = this._el.childNodes;
- for (let i = 0; i < children.length; i++) {
- let c = this._el.childNodes[i];
- if (c.tagName == "spacer")
- this._addNewBarrierFromSpacer(c);
- else
- this._addNewWidget(c);
- }
- // this also updates the viewportOverflow and pannableBounds
- this._updateWidgets();
- if (this._viewport) {
- this._viewportBounds = new wsRect(0, 0, this._viewport.rect.width, this._viewport.rect.height);
- } else {
- this._viewportBounds = new wsRect(0, 0, 0, 0);
- }
- },
- // moveWidgetBy: move the widget with the given id by x,y. Should
- // not be used on vp-relative or otherwise frozen widgets (using it
- // on the x coordinate for x-ignore widgets and similarily for y is
- // ok, as long as the other coordinate remains 0.)
- moveWidgetBy: function (wid, x, y) {
- let state = this._getState(wid);
- state.rect.x += x;
- state.rect.y += y;
- this._commitState(state);
- },
- // panBy: pan the entire set of widgets by the given x and y amounts.
- // This does the same thing as if the user dragged by the given amount.
- // If this is called with an outstanding drag, weirdness might happen,
- // but it also might work, so not disabling that.
- //
- // if ignoreBarriers is true, then barriers are ignored for the pan.
- panBy: function panBy(dx, dy, ignoreBarriers) {
- dx = Math.round(dx);
- dy = Math.round(dy);
- if (dx == 0 && dy == 0)
- return false;
- let needsDragWrap = !this._dragging;
- if (needsDragWrap)
- this.dragStart(0, 0);
- let panned = this._panBy(dx, dy, ignoreBarriers);
- if (needsDragWrap)
- this.dragStop();
- return panned;
- },
- // panTo: pan the entire set of widgets so that the given x,y
- // coordinates are in the upper left of the stack. If either is
- // null or undefined, only move the other axis
- panTo: function panTo(x, y) {
- if (x == undefined || x == null)
- x = this._viewingRect.x;
- if (y == undefined || y == null)
- y = this._viewingRect.y;
- this.panBy(x - this._viewingRect.x, y - this._viewingRect.y, true);
- },
- // freeze: set a widget as frozen. A frozen widget won't be moved
- // in the stack -- its x,y position will still be tracked in the
- // state, but the left/top attributes won't be overwritten. Call unfreeze
- // to move the widget back to where the ws thinks it should be.
- freeze: function (wid) {
- let state = this._getState(wid);
- state.frozen = true;
- },
- unfreeze: function (wid) {
- let state = this._getState(wid);
- if (!state.frozen)
- return;
- state.frozen = false;
- this._commitState(state);
- },
- // moveFrozenTo: move a frozen widget with id wid to x, y in the stack.
- // can only be used on frozen widgets
- moveFrozenTo: function (wid, x, y) {
- let state = this._getState(wid);
- if (!state.frozen)
- throw "moveFrozenTo on non-frozen widget " + wid;
- state.widget.setAttribute("left", x);
- state.widget.setAttribute("top", y);
- },
- // moveUnfrozenTo: move an unfrozen, pannable widget with id wid to x, y in
- // the stack. should only be used on unfrozen widgets when a dynamic change
- // in position needs to be made. we basically remove, adjust and re-add
- // the widget
- moveUnfrozenTo: function (wid, x, y) {
- delete this._widgetState[wid];
- let widget = document.getElementById(wid);
- if (x) widget.setAttribute("left", x);
- if (y) widget.setAttribute("top", y);
- this._addNewWidget(widget);
- this._updateWidgets();
- },
- // we're relying on viewportBounds and viewingRect having the same origin
- get viewportVisibleRect () {
- let rect = this._viewportBounds.intersect(this._viewingRect);
- if (!rect)
- rect = new wsRect(0, 0, 0, 0);
- return rect;
- },
- isWidgetFrozen: function isWidgetFrozen(wid) {
- return this._getState(wid).frozen;
- },
- // isWidgetVisible: return true if any portion of widget with id wid is
- // visible; otherwise return false.
- isWidgetVisible: function (wid) {
- let state = this._getState(wid);
- let visibleStackRect = new wsRect(0, 0, this._viewingRect.width, this._viewingRect.height);
- return visibleStackRect.intersects(state.rect);
- },
- // getWidgetVisibility: returns the percentage that the widget is visible
- getWidgetVisibility: function (wid) {
- let state = this._getState(wid);
- let visibleStackRect = new wsRect(0, 0, this._viewingRect.width, this._viewingRect.height);
- let visibleRect = visibleStackRect.intersect(state.rect);
- if (visibleRect)
- return [visibleRect.width / state.rect.width, visibleRect.height / state.rect.height]
- return [0, 0];
- },
- // offsetAll: add an offset to all widgets
- offsetAll: function (x, y) {
- this.globalOffsetX += x;
- this.globalOffsetY += y;
- for each (let state in this._widgetState) {
- state.rect.x += x;
- state.rect.y += y;
- this._commitState(state);
- }
- },
- // setViewportBounds
- // nb: an object containing top, left, bottom, right properties
- // OR
- // width, height: integer values; origin assumed to be 0,0
- // OR
- // top, left, bottom, right: integer values
- //
- // Set the bounds of the viewport area; that is, set the size of the
- // actual content that the viewport widget will be providing a view
- // over. For example, in the case of a 100x100 viewport showing a
- // view into a 100x500 webpage, the viewport bounds would be
- // { top: 0, left: 0, bottom: 500, right: 100 }.
- //
- // setViewportBounds will move all the viewport-relative widgets into
- // place based on the new viewport bounds.
- setViewportBounds: function setViewportBounds() {
- let oldBounds = this._viewportBounds.clone();
- if (arguments.length == 1) {
- this._viewportBounds.copyFromTLBR(arguments[0]);
- } else if (arguments.length == 2) {
- this._viewportBounds.setRect(0, 0, arguments[0], arguments[1]);
- } else if (arguments.length == 4) {
- this._viewportBounds.setBounds(arguments[0],
- arguments[1],
- arguments[2],
- arguments[3]);
- } else {
- throw "Invalid number of arguments to setViewportBounds";
- }
- let vp = this._viewport;
- let dleft = this._viewportBounds.left - oldBounds.left;
- let dright = this._viewportBounds.right - oldBounds.right;
- let dtop = this._viewportBounds.top - oldBounds.top;
- let dbottom = this._viewportBounds.bottom - oldBounds.bottom;
- //log2("setViewportBounds dltrb", dleft, dtop, dright, dbottom);
- // move all vp-relative widgets to be the right offset from the bounds again
- for each (let state in this._widgetState) {
- if (state.vpRelative) {
- //log2("vpRelative widget", state.id, state.rect.x, dleft, dright);
- if (state.vpOffsetXBefore) {
- state.rect.x += dleft;
- } else {
- state.rect.x += dright;
- }
- if (state.vpOffsetYBefore) {
- state.rect.y += dtop;
- } else {
- state.rect.y += dbottom;
- }
- //log2("vpRelative widget", state.id, state.rect.x, dleft, dright);
- this._commitState(state);
- }
- }
- for (let bid in this._barriers) {
- let barrier = this._barriers[bid];
- //log2("setViewportBounds: looking at barrier", bid, barrier.vpRelative, barrier.type);
- if (barrier.vpRelative) {
- if (barrier.type == "vertical") {
- let q = "v barrier moving from " + barrier.x + " to ";
- if (barrier.vpOffsetXBefore) {
- barrier.x += dleft;
- } else {
- barrier.x += dright;
- }
- //log2(q += barrier.x);
- } else if (barrier.type == "horizontal") {
- let q = "h barrier moving from " + barrier.y + " to ";
- if (barrier.vpOffsetYBefore) {
- barrier.y += dtop;
- } else {
- barrier.y += dbottom;
- }
- //log2(q += barrier.y);
- }
- }
- }
- // clear the pannable bounds cache to make sure it gets rebuilt
- this._pannableBounds = null;
- // now let's make sure that the viewing rect and inner bounds are still valid
- this._adjustViewingRect();
- this._viewportUpdate(0, 0, true);
- },
- // setViewportHandler
- // uh: A function object
- //
- // The given function object is called at the end of every drag and viewport
- // bounds change, passing in the new rect that's to be displayed in the
- // viewport.
- //
- setViewportHandler: function (uh) {
- this._viewportUpdateHandler = uh;
- },
- // setPanHandler
- // uh: A function object
- //
- // The given functin object is called whenever elements pan; it provides
- // the new area of the pannable bounds that's visible in the stack.
- setPanHandler: function (uh) {
- this._panHandler = uh;
- },
- // dragStart: start a drag, with the current coordinates being clientX,clientY
- dragStart: function dragStart(clientX, clientY) {
- log("(dragStart)", clientX, clientY);
- if (this._dragState) {
- reportError("dragStart with drag already in progress? what?");
- this._dragState = null;
- }
- this._dragState = { };
- let t = Date.now();
- this._dragState.barrierState = [];
- this._dragState.startTime = t;
- // outer x, that is outer from the viewport coordinates. In stack-relative coords.
- this._dragState.outerStartX = clientX;
- this._dragState.outerStartY = clientY;
- this._dragCoordsFromClient(clientX, clientY, t);
- this._dragState.outerLastUpdateDX = 0;
- this._dragState.outerLastUpdateDY = 0;
- if (this._viewport) {
- // create a copy of these so that we can compute
- // deltas correctly to update the viewport
- this._viewport.dragStartRect = this._viewport.rect.clone();
- }
- this._dragState.dragging = true;
- },
- _viewportDragUpdate: function viewportDragUpdate() {
- let vws = this._viewport;
- this._viewportUpdate((vws.dragStartRect.x - vws.rect.x),
- (vws.dragStartRect.y - vws.rect.y));
- },
- // dragStop: stop any drag in progress
- dragStop: function dragStop() {
- log("(dragStop)");
- if (!this._dragging)
- return;
- if (this._viewportUpdateTimeout != -1)
- clearTimeout(this._viewportUpdateTimeout);
- this._viewportDragUpdate();
- this._dragState = null;
- },
- // dragMove: process a mouse move to clientX,clientY for an ongoing drag
- dragMove: function dragMove(clientX, clientY) {
- if (!this._dragging)
- return false;
- this._dragCoordsFromClient(clientX, clientY);
- let panned = this._dragUpdate();
- if (this._viewportUpdateInterval != -1) {
- if (this._viewportUpdateTimeout != -1)
- clearTimeout(this._viewportUpdateTimeout);
- let self = this;
- this._viewportUpdateTimeout = setTimeout(function () { self._viewportDragUpdate(); }, this._viewportUpdateInterval);
- }
- return panned;
- },
- // dragBy: process a mouse move by dx,dy for an ongoing drag
- dragBy: function dragBy(dx, dy) {
- return this.dragMove(this._dragState.outerCurX + dx, this._dragState.outerCurY + dy);
- },
- // updateSize: tell the WidgetStack to update its size, because it
- // was either resized or some other event took place.
- updateSize: function updateSize(width, height) {
- if (width == undefined || height == undefined) {
- let rect = this._el.getBoundingClientRect();
- width = rect.width;
- height = rect.height;
- }
- // update widget rects and viewportOverflow, since the resize might have
- // caused them to change (widgets first, since the viewportOverflow depends
- // on them).
- // XXX these methods aren't working correctly yet, but they aren't strictly
- // necessary in Fennec's default config
- //for each (let s in this._widgetState)
- // this._updateWidgetRect(s);
- //this._updateViewportOverflow();
- this._viewingRect.width = width;
- this._viewingRect.height = height;
- // Wrap this call in a batch to ensure that we always call the
- // viewportUpdateHandler, even if _adjustViewingRect doesn't trigger a pan.
- // If it does, the batch also ensures that we don't call the handler twice.
- this.beginUpdateBatch();
- this._adjustViewingRect();
- this.endUpdateBatch();
- },
- beginUpdateBatch: function startUpdate() {
- if (!this._skipViewportUpdates) {
- this._startViewportBoundsString = this._viewportBounds.toString();
- this._forceViewportUpdate = false;
- }
- this._skipViewportUpdates++;
- },
- endUpdateBatch: function endUpdate(aForceRedraw) {
- if (!this._skipViewportUpdates)
- throw new Error("Unbalanced call to endUpdateBatch");
- this._forceViewportUpdate = this._forceViewportUpdate || aForceRedraw;
- this._skipViewportUpdates--;
- if (this._skipViewportUpdates)
- return;
- let boundsSizeChanged =
- this._startViewportBoundsString != this._viewportBounds.toString();
- this._callViewportUpdateHandler(boundsSizeChanged || this._forceViewportUpdate);
- },
- //
- // Internal code
- //
- _updateWidgetRect: function(state) {
- // don't need to support updating the viewport rect at the moment
- // (we'd need to duplicate the vptarget* code from _addNewWidget if we did)
- if (state == this._viewport)
- return;
- let w = state.widget;
- let x = w.getAttribute("left") || 0;
- let y = w.getAttribute("top") || 0;
- let rect = w.getBoundingClientRect();
- state.rect = new wsRect(parseInt(x), parseInt(y),
- rect.right - rect.left,
- rect.bottom - rect.top);
- if (w.hasAttribute("widgetwidth") && w.hasAttribute("widgetheight")) {
- state.rect.width = parseInt(w.getAttribute("widgetwidth"));
- state.rect.height = parseInt(w.getAttribute("widgetheight"));
- }
- },
- _dumpRects: function () {
- dump("WidgetStack:\n");
- dump("\tthis._viewportBounds: " + this._viewportBounds + "\n");
- dump("\tthis._viewingRect: " + this._viewingRect + "\n");
- dump("\tthis._viewport.viewportInnerBounds: " + this._viewport.viewportInnerBounds + "\n");
- dump("\tthis._viewport.rect: " + this._viewport.rect + "\n");
- dump("\tthis._viewportOverflow: " + this._viewportOverflow + "\n");
- dump("\tthis.pannableBounds: " + this.pannableBounds + "\n");
- },
- // Ensures that _viewingRect is within _pannableBounds (call this when either
- // one is resized)
- _adjustViewingRect: function _adjustViewingRect() {
- let vr = this._viewingRect;
- let pb = this.pannableBounds;
- if (pb.contains(vr))
- return; // nothing to do here
- // don't bother adjusting _viewingRect if it can't fit into
- // _pannableBounds
- if (vr.height > pb.height || vr.width > pb.width)
- return;
- let panX = 0, panY = 0;
- if (vr.right > pb.right)
- panX = pb.right - vr.right;
- else if (vr.left < pb.left)
- panX = pb.left - vr.left;
- if (vr.bottom > pb.bottom)
- panY = pb.bottom - vr.bottom;
- else if(vr.top < pb.top)
- panY = pb.top - vr.top;
- this.panBy(panX, panY, true);
- },
- _getState: function (wid) {
- let w = this._widgetState[wid];
- if (!w)
- throw "Unknown widget id '" + wid + "'; widget not in stack";
- return w;
- },
- get _dragging() {
- return this._dragState && this._dragState.dragging;
- },
- _viewportUpdate: function _viewportUpdate(dX, dY, boundsChanged) {
- if (!this._viewport)
- return;
- this._viewportUpdateTimeout = -1;
- let vws = this._viewport;
- let vwib = vws.viewportInnerBounds;
- let vpb = this._viewportBounds;
- // recover the amount the inner bounds moved by the amount the viewport
- // widget moved, but don't include offsets that we're making up from previous
- // drags that didn't affect viewportInnerBounds
- let [ignoreX, ignoreY] = this._offsets || [0, 0];
- let rx = dX - ignoreX;
- let ry = dY - ignoreY;
- [dX, dY] = this._rectTranslateConstrain(rx, ry, vwib, vpb);
- // record the offsets that correspond to the amount of the drag we're ignoring
- // to ensure the viewportInnerBounds remains within the viewportBounds
- this._offsets = [dX - rx, dY - ry];
- // adjust the viewportInnerBounds, and snap the viewport back
- vwib.translate(dX, dY);
- vws.rect.translate(dX, dY);
- this._commitState(vws);
- // update this so that we can call this function again during the same drag
- // and get the right values.
- vws.dragStartRect = vws.rect.clone();
- this._callViewportUpdateHandler(boundsChanged);
- },
- _callViewportUpdateHandler: function _callViewportUpdateHandler(boundsChanged) {
- if (!this._viewport || !this._viewportUpdateHandler || this._skipViewportUpdates)
- return;
- let vwb = this._viewportBounds.clone();
- let vwib = this._viewport.viewportInnerBounds.clone();
- let vis = this.viewportVisibleRect;
- vwib.left += this._viewport.offsetLeft;
- vwib.top += this._viewport.offsetTop;
- vwib.right += this._viewport.offsetRight;
- vwib.bottom += this._viewport.offsetBottom;
- this._viewportUpdateHandler.apply(window, [vwb, vwib, vis, boundsChanged]);
- },
- _dragCoordsFromClient: function (cx, cy, t) {
- this._dragState.curTime = t ? t : Date.now();
- this._dragState.outerCurX = cx;
- this._dragState.outerCurY = cy;
- let dx = this._dragState.outerCurX - this._dragState.outerStartX;
- let dy = this._dragState.outerCurY - this._dragState.outerStartY;
- this._dragState.outerDX = dx;
- this._dragState.outerDY = dy;
- },
- _panHandleBarriers: function (dx, dy) {
- // XXX unless the barriers are sorted by position, this will break
- // with multiple barriers that are near enough to eachother that a
- // drag could cross more than one.
- let vr = this._viewingRect;
- // XXX this just stops at the first horizontal and vertical barrier it finds
- // barrier_[xy] is the barrier that was used to get to the final
- // barrier_d[xy] value. if null, no barrier, and dx/dy shouldn't
- // be replaced with barrier_d[xy].
- let barrier_y = null, barrier_x = null;
- let barrier_dy = 0, barrier_dx = 0;
- for (let i = 0; i < this._barriers.length; i++) {
- let b = this._barriers[i];
- //log2("barrier", i, b.type, b.x, b.y);
- if (dx != 0 && b.type == "vertical") {
- if (barrier_x != null) {
- delete this._dragState.barrierState[i];
- continue;
- }
- let alreadyKnownDistance = this._dragState.barrierState[i] || 0;
- //log2("alreadyKnownDistance", alreadyKnownDistance);
- let dbx = 0;
- //100 <= 100 && 100-(-5) > 100
- if ((vr.left <= b.x && vr.left+dx > b.x) ||
- (vr.left >= b.x && vr.left+dx < b.x))
- {
- dbx = b.x - vr.left;
- } else if ((vr.right <= b.x && vr.right+dx > b.x) ||
- (vr.right >= b.x && vr.right+dx < b.x))
- {
- dbx = b.x - vr.right;
- } else {
- delete this._dragState.barrierState[i];
- continue;
- }
- let leftoverDistance = dbx - dx;
- //log2("initial dbx", dbx, leftoverDistance);
- let dist = Math.abs(leftoverDistance + alreadyKnownDistance) - b.size;
- if (dist >= 0) {
- if (dx < 0)
- dbx -= dist;
- else
- dbx += dist;
- delete this._dragState.barrierState[i];
- } else {
- dbx = 0;
- this._dragState.barrierState[i] = leftoverDistance + alreadyKnownDistance;
- }
- //log2("final dbx", dbx, "state", this._dragState.barrierState[i]);
- if (Math.abs(barrier_dx) <= Math.abs(dbx)) {
- barrier_x = b;
- barrier_dx = dbx;
- //log2("new barrier_dx", barrier_dx);
- }
- }
- if (dy != 0 && b.type == "horizontal") {
- if (barrier_y != null) {
- delete this._dragState.barrierState[i];
- continue;
- }
- let alreadyKnownDistance = this._dragState.barrierState[i] || 0;
- //log2("alreadyKnownDistance", alreadyKnownDistance);
- let dby = 0;
- //100 <= 100 && 100-(-5) > 100
- if ((vr.top <= b.y && vr.top+dy > b.y) ||
- (vr.top >= b.y && vr.top+dy < b.y))
- {
- dby = b.y - vr.top;
- } else if ((vr.bottom <= b.y && vr.bottom+dy > b.y) ||
- (vr.bottom >= b.y && vr.bottom+dy < b.y))
- {
- dby = b.y - vr.bottom;
- } else {
- delete this._dragState.barrierState[i];
- continue;
- }
- let leftoverDistance = dby - dy;
- //log2("initial dby", dby, leftoverDistance);
- let dist = Math.abs(leftoverDistance + alreadyKnownDistance) - b.size;
- if (dist >= 0) {
- if (dy < 0)
- dby -= dist;
- else
- dby += dist;
- delete this._dragState.barrierState[i];
- } else {
- dby = 0;
- this._dragState.barrierState[i] = leftoverDistance + alreadyKnownDistance;
- }
- //log2("final dby", dby, "state", this._dragState.barrierState[i]);
- if (Math.abs(barrier_dy) <= Math.abs(dby)) {
- barrier_y = b;
- barrier_dy = dby;
- //log2("new barrier_dy", barrier_dy);
- }
- }
- }
- if (barrier_x) {
- //log2("did barrier_x", barrier_x, "barrier_dx", barrier_dx);
- dx = barrier_dx;
- }
- if (barrier_y) {
- dy = barrier_dy;
- }
- return [dx, dy];
- },
- _panBy: function _panBy(dx, dy, ignoreBarriers) {
- let vr = this._viewingRect;
- // check if any barriers would be crossed by this pan, and take them
- // into account. do this first.
- if (!ignoreBarriers)
- [dx, dy] = this._panHandleBarriers(dx, dy);
- // constrain the full drag of the viewingRect to the pannableBounds.
- // note that the viewingRect needs to move in the opposite
- // direction of the pan, so we fiddle with the signs here (as you
- // pan to the upper left, more of the bottom right becomes visible,
- // so the viewing rect moves to the bottom right of the virtual surface).
- [dx, dy] = this._rectTranslateConstrain(dx, dy, vr, this.pannableBounds);
- // If the net result is that we don't have any room to move, then
- // just return.
- if (dx == 0 && dy == 0)
- return false;
- // the viewingRect moves opposite of the actual pan direction, see above
- vr.x += dx;
- vr.y += dy;
- // Go through each widget and move it by dx,dy. Frozen widgets
- // will be ignored in commitState.
- // The widget rects are in real stack space though, so we need to subtract
- // our (now negated) dx, dy from their coordinates.
- for each (let state in this._widgetState) {
- if (!state.ignoreX)
- state.rect.x -= dx;
- if (!state.ignoreY)
- state.rect.y -= dy;
- this._commitState(state);
- }
- /* Do not call panhandler during pans within a transaction.
- * Those pans always end-up covering up the checkerboard and
- * do not require sliding out the location bar
- */
- if (!this._skipViewportUpdates && this._panHandler)
- this._panHandler.apply(window, [vr.clone(), dx, dy]);
- return true;
- },
- _dragUpdate: function _dragUpdate() {
- let dx = this._dragState.outerLastUpdateDX - this._dragState.outerDX;
- let dy = this._dragState.outerLastUpdateDY - this._dragState.outerDY;
- this._dragState.outerLastUpdateDX = this._dragState.outerDX;
- this._dragState.outerLastUpdateDY = this._dragState.outerDY;
- return this.panBy(dx, dy);
- },
- //
- // widget addition/removal
- //
- _addNewWidget: function (w) {
- let wid = w.getAttribute("id");
- if (!wid) {
- reportError("WidgetStack: child widget without id!");
- return;
- }
- if (w.getAttribute("hidden") == "true")
- return;
- let state = {
- widget: w,
- id: wid,
- viewport: false,
- ignoreX: false,
- ignoreY: false,
- sticky: false,
- frozen: false,
- vpRelative: false,
- offsetLeft: 0,
- offsetTop: 0,
- offsetRight: 0,
- offsetBottom: 0
- };
- this._updateWidgetRect(state);
- if (w.hasAttribute("constraint")) {
- let cs = w.getAttribute("constraint").split(",");
- for each (let s in cs) {
- if (s == "ignore-x")
- state.ignoreX = true;
- else if (s == "ignore-y")
- state.ignoreY = true;
- else if (s == "sticky")
- state.sticky = true;
- else if (s == "frozen") {
- state.frozen = true;
- } else if (s == "vp-relative")
- state.vpRelative = true;
- }
- }
- if (w.hasAttribute("viewport")) {
- if (this._viewport)
- reportError("WidgetStack: more than one viewport canvas in stack!");
- this._viewport = state;
- state.viewport = true;
- if (w.hasAttribute("vptargetx") && w.hasAttribute("vptargety") &&
- w.hasAttribute("vptargetw") && w.hasAttribute("vptargeth"))
- {
- let wx = parseInt(w.getAttribute("vptargetx"));
- let wy = parseInt(w.getAttribute("vptargety"));
- let ww = parseInt(w.getAttribute("vptargetw"));
- let wh = parseInt(w.getAttribute("vptargeth"));
- state.offsetLeft = state.rect.left - wx;
- state.offsetTop = state.rect.top - wy;
- state.offsetRight = state.rect.right - (wx + ww);
- state.offsetBottom = state.rect.bottom - (wy + wh);
- state.rect = new wsRect(wx, wy, ww, wh);
- }
- // initialize inner bounds to top-left
- state.viewportInnerBounds = new wsRect(0, 0, state.rect.width, state.rect.height);
- }
- this._widgetState[wid] = state;
- log ("(New widget: " + wid + (state.viewport ? " [viewport]" : "") + " at: " + state.rect + ")");
- },
- _removeWidget: function (w) {
- let wid = w.getAttribute("id");
- delete this._widgetState[wid];
- this._updateWidgets();
- },
- // updateWidgets:
- // Go through all the widgets and figure out their viewport-relative offsets.
- // If the widget goes to the left or above the viewport widget, then
- // vpOffsetXBefore or vpOffsetYBefore is set.
- // See setViewportBounds for use of vpOffset* state variables, and for how
- // the actual x and y coords of each widget are calculated based on their offsets
- // and the viewport bounds.
- _updateWidgets: function () {
- let vp = this._viewport;
- let ofRect = this._viewingRect.clone();
- for each (let state in this._widgetState) {
- if (vp && state.vpRelative) {
- // compute the vpOffset from 0,0 assuming that the viewport rect is 0,0
- if (state.rect.left >= vp.rect.right) {
- state.vpOffsetXBefore = false;
- state.vpOffsetX = state.rect.left - vp.rect.width;
- } else {
- state.vpOffsetXBefore = true;
- state.vpOffsetX = state.rect.left - vp.rect.left;
- }
- if (state.rect.top >= vp.rect.bottom) {
- state.vpOffsetYBefore = false;
- state.vpOffsetY = state.rect.top - vp.rect.height;
- } else {
- state.vpOffsetYBefore = true;
- state.vpOffsetY = state.rect.top - vp.rect.top;
- }
- log("widget", state.id, "offset", state.vpOffsetX, state.vpOffsetXBefore ? "b" : "a", state.vpOffsetY, state.vpOffsetYBefore ? "b" : "a", "rect", state.rect);
- }
- }
- this._updateViewportOverflow();
- },
- // updates the viewportOverflow/pannableBounds
- _updateViewportOverflow: function() {
- let vp = this._viewport;
- if (!vp)
- return;
- let ofRect = new wsRect(0, 0, this._viewingRect.width, this._viewingRect.height);
- for each (let state in this._widgetState) {
- if (vp && state.vpRelative) {
- ofRect.left = Math.min(ofRect.left, state.rect.left);
- ofRect.top = Math.min(ofRect.top, state.rect.top);
- ofRect.right = Math.max(ofRect.right, state.rect.right);
- ofRect.bottom = Math.max(ofRect.bottom, state.rect.bottom);
- }
- }
- // prevent the viewportOverflow from having positive top/left or negative
- // bottom/right values, which would otherwise happen if there aren't widgets
- // beyond each of those edges
- this._viewportOverflow = new wsBorder(
- /*top*/ Math.round(Math.min(ofRect.top, 0)),
- /*left*/ Math.round(Math.min(ofRect.left, 0)),
- /*bottom*/ Math.round(Math.max(ofRect.bottom - vp.rect.height, 0)),
- /*right*/ Math.round(Math.max(ofRect.right - vp.rect.width, 0))
- );
- // clear the _pannableBounds cache, since it depends on the
- // viewportOverflow
- this._pannableBounds = null;
- },
- _widgetBounds: function () {
- let r = new wsRect(0,0,0,0);
- for each (let state in this._widgetState)
- r = r.union(state.rect);
- return r;
- },
- _commitState: function (state) {
- // if the widget is frozen, don't actually update its left/top;
- // presumably the caller is managing those directly for now.
- if (state.frozen)
- return;
- let w = state.widget;
- let l = state.rect.x + state.offsetLeft;
- let t = state.rect.y + state.offsetTop;
- //cache left/top to avoid calling setAttribute unnessesarily
- if (state._left != l) {
- state._left = l;
- w.setAttribute("left", l);
- }
- if (state._top != t) {
- state._top = t;
- w.setAttribute("top", t);
- }
- },
- // constrain translate of rect by dx dy to bounds; return dx dy that can
- // be used to bring rect up to the edge of bounds if we'd go over.
- _rectTranslateConstrain: function (dx, dy, rect, bounds) {
- let newX, newY;
- // If the rect is larger than the bounds, allow it to increase its overlap
- let woverflow = rect.width > bounds.width;
- let hoverflow = rect.height > bounds.height;
- if (woverflow || hoverflow) {
- let intersection = rect.intersect(bounds);
- let newIntersection = rect.clone().translate(dx, dy).intersect(bounds);
- if (woverflow)
- newX = (newIntersection.width > intersection.width) ? rect.x + dx : rect.x;
- if (hoverflow)
- newY = (newIntersection.height > intersection.height) ? rect.y + dy : rect.y;
- }
- // Common case, rect fits within the bounds
- // clamp new X to within [bounds.left, bounds.right - rect.width],
- // new Y to within [bounds.top, bounds.bottom - rect.height]
- if (isNaN(newX))
- newX = Math.min(Math.max(bounds.left, rect.x + dx), bounds.right - rect.width);
- if (isNaN(newY))
- newY = Math.min(Math.max(bounds.top, rect.y + dy), bounds.bottom - rect.height);
- return [newX - rect.x, newY - rect.y];
- },
- // add a new barrier from a <spacer>
- _addNewBarrierFromSpacer: function (el) {
- let t = el.getAttribute("barriertype");
- // XXX implement these at some point
- // t != "lr" && t != "rl" &&
- // t != "tb" && t != "bt" &&
- if (t != "horizontal" &&
- t != "vertical")
- {
- throw "Invalid barrier type: " + t;
- }
- let x, y;
- let barrier = {};
- let vp = this._viewport;
- barrier.type = t;
- if (el.getAttribute("left"))
- barrier.x = parseInt(el.getAttribute("left"));
- else if (el.getAttribute("top"))
- barrier.y = parseInt(el.getAttribute("top"));
- else
- throw "Barrier without top or left attribute";
- if (el.getAttribute("size"))
- barrier.size = parseInt(el.getAttribute("size"));
- else
- barrier.size = 10;
- if (el.hasAttribute("constraint")) {
- let cs = el.getAttribute("constraint").split(",");
- for each (let s in cs) {
- if (s == "ignore-x")
- barrier.ignoreX = true;
- else if (s == "ignore-y")
- barrier.ignoreY = true;
- else if (s == "sticky")
- barrier.sticky = true;
- else if (s == "frozen") {
- barrier.frozen = true;
- } else if (s == "vp-relative")
- barrier.vpRelative = true;
- }
- }
- if (barrier.vpRelative) {
- if (barrier.type == "vertical") {
- if (barrier.x >= vp.rect.right) {
- barrier.vpOffsetXBefore = false;
- barrier.vpOffsetX = barrier.x - vp.rect.right;
- } else {
- barrier.vpOffsetXBefore = true;
- barrier.vpOffsetX = barrier.x - vp.rect.left;
- }
- } else if (barrier.type == "horizontal") {
- if (barrier.y >= vp.rect.bottom) {
- barrier.vpOffsetYBefore = false;
- barrier.vpOffsetY = barrier.y - vp.rect.bottom;
- } else {
- barrier.vpOffsetYBefore = true;
- barrier.vpOffsetY = barrier.y - vp.rect.top;
- }
- //log2("h barrier relative", barrier.vpOffsetYBefore, barrier.vpOffsetY);
- }
- }
- this._barriers.push(barrier);
- }
- };