/* -*- 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 * * 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 = { _t: 0, _l: 0, _b: 0, _r: 0, get left() { return this._l; }, get right() { return this._r; }, get top() { return this._t; }, get bottom() { return this._b; }, set left(v) { this._l = v; }, set right(v) { this._r = v; }, set top(v) { this._t = v; }, set bottom(v) { this._b = v; }, setBorder: function (t, l, b, r) { this._t = t; this._l = l; this._b = b; this._r = r; }, toString: function () { return "[l:" + this._l + ",t:" + this._t + ",r:" + this._r + ",b:" + this._b + "]"; } }; /* * wsRect class * * Rectangle class, with both x/y/w/h and t/l/b/r accessors. */ function wsRect(x, y, w, h) { this.setRect(x, y, w, h); } wsRect.prototype = { _l: 0, _t: 0, _b: 0, _r: 0, get x() { return this._l; }, get y() { return this._t; }, get width() { return this._r - this._l; }, get height() { return this._b - this._t; }, set x(v) { let diff = this._l - v; this._l = v; this._r -= diff; }, set y(v) { let diff = this._t - v; this._t = v; this._b -= diff; }, set width(v) { this._r = this._l + v; }, set height(v) { this._b = this._t + v; }, get left() { return this._l; }, get right() { return this._r; }, get top() { return this._t; }, get bottom() { return this._b; }, set left(v) { this._l = v; }, set right(v) { this._r = v; }, set top(v) { this._t = v; }, set bottom(v) { this._b = v; }, setRect: function(x, y, w, h) { this._l = x; this._t = y; this._r = x+w; this._b = y+h; return this; }, setBounds: function(t, l, b, r) { this._t = t; this._l = l; this._b = b; this._r = r; return this; }, clone: function() { return new wsRect(this._l, this._t, this.width, this.height); }, copyFrom: function(r) { this._t = r._t; this._l = r._l; this._b = r._b; this._r = r._r; return this; }, copyFromTLBR: function(r) { this._l = r.left; this._t = r.top; this._r = r.right; this._b = r.bottom; return this; }, translate: function(x, y) { this._l += x; this._r += x; this._t += y; this._b += 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._l, rect._l); let r = Math.max(this._r, rect._r); let t = Math.min(this._t, rect._t); let b = Math.max(this._b, rect._b); return new wsRect(l, t, r-l, b-t); }, toString: function() { return "[" + this.x + "," + this.y + "," + this.width + "," + this.height + "]"; }, expandBy: function(b) { this._l += b.left; this._r += b.right; this._t += b.top; this._b += b.bottom; return this; }, contains: function(other) { return !!(other._l >= this._l && other._r <= this._r && other._t >= this._t && other._b <= this._b); }, intersect: function(r2) { let xmost1 = this._r; let xmost2 = r2._r; let x = Math.max(this._l, r2._l); let temp = Math.min(xmost1, xmost2); if (temp <= x) return null; let width = temp - x; let ymost1 = this._b; let ymost2 = r2._b; let y = Math.max(this._t, r2._t); 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._l > this._l && other._l < this._r) || (other._r > this._l && other._r < this._r) || (other._l <= this._l && other._r >= this._r); let yok = (other._t > this._t && other._t < this._b) || (other._b > this._t && other._b < this._b) || (other._t <= this._t && other._b >= this._b); return xok && yok; }, round: function(scale) { this._l = Math.floor(this._l * scale) / scale; this._t = Math.floor(this._t * scale) / scale; this._r = Math.ceil(this._r * scale) / scale; this._b = Math.ceil(this._b * scale) / scale; } }; /* * The "Widget Stack" * * Manages a '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 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, // // init: // el: the 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) { 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); }, // 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; }, // 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._callViewportUpdateHandler(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; }, // dragStop: stop any drag in progress dragStop: function dragStop() { log("(dragStop)"); if (!this._dragging) return; if (this._viewportUpdateTimeout != -1) clearTimeout(this._viewportUpdateTimeout); this._viewportUpdate(); 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._viewportUpdate(); }, 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._skipViewportUpdates++; }, endUpdateBatch: function endUpdate() { if (!this._skipViewportUpdates) throw new Error("Unbalanced call to endUpdateBatch"); this._skipViewportUpdates--; if (this._skipViewportUpdates) return let boundsSizeChanged = this._startViewportBoundsString != this._viewportBounds.toString(); this._callViewportUpdateHandler(boundsSizeChanged); }, // // 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.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() { 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 = (vws.dragStartRect.x - vws.rect.x) - ignoreX; let ry = (vws.dragStartRect.y - vws.rect.y) - ignoreY; let [dX, dY] = this._rectTranslateConstrain(rx, ry, vwib, vpb); // record the offsets that correpond 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(false); }, _callViewportUpdateHandler: function _callViewportUpdateHandler(boundsChanged) { if (!this._viewport || !this._viewportUpdateHandler || this._skipViewportUpdates) return; let vwib = this._viewport.viewportInnerBounds.clone(); vwib.left += this._viewport.offsetLeft; vwib.top += this._viewport.offsetTop; vwib.right += this._viewport.offsetRight; vwib.bottom += this._viewport.offsetBottom; this._viewportUpdateHandler.apply(window, [vwib, 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.min(ofRect.top, 0), /*left*/ Math.min(ofRect.left, 0), /*bottom*/ Math.max(ofRect.bottom - vp.rect.height, 0), /*right*/ 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 _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); } };