diff -r 000000000000 -r 6474c204b198 browser/components/tabview/trench.js --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/browser/components/tabview/trench.js Wed Dec 31 06:09:35 2014 +0100 @@ -0,0 +1,658 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +// ********** +// Title: trench.js + +// ########## +// Class: Trench +// +// Class for drag-snapping regions; called "trenches" as they are long and narrow. + +// Constructor: Trench +// +// Parameters: +// element - the DOM element for Item (GroupItem or TabItem) from which the trench is projected +// xory - either "x" or "y": whether the trench's is along the x- or y-axis. +// In other words, if "x", the trench is vertical; if "y", the trench is horizontal. +// type - either "border" or "guide". Border trenches mark the border of an Item. +// Guide trenches extend out (unless they are intercepted) and act as "guides". +// edge - which edge of the Item that this trench corresponds to. +// Either "top", "left", "bottom", or "right". +function Trench(element, xory, type, edge) { + //---------- + // Variable: id + // (integer) The id for the Trench. Set sequentially via + this.id = Trenches.nextId++; + + // --------- + // Variables: Initial parameters + // element - (DOMElement) + // parentItem - which projects this trench; to be set with setParentItem + // xory - (string) "x" or "y" + // type - (string) "border" or "guide" + // edge - (string) "top", "left", "bottom", or "right" + this.el = element; + this.parentItem = null; + this.xory = xory; // either "x" or "y" + this.type = type; // "border" or "guide" + this.edge = edge; // "top", "left", "bottom", or "right" + + this.$el = iQ(this.el); + + //---------- + // Variable: dom + // (array) DOM elements for visible reflexes of the Trench + this.dom = []; + + //---------- + // Variable: showGuide + // (boolean) Whether this trench will project a visible guide (dotted line) or not. + this.showGuide = false; + + //---------- + // Variable: active + // (boolean) Whether this trench is currently active or not. + // Basically every trench aside for those projected by the Item currently being dragged + // all become active. + this.active = false; + this.gutter = Items.defaultGutter; + + //---------- + // Variable: position + // (integer) position is the position that we should snap to. + this.position = 0; + + //---------- + // Variables: some Ranges + // range - () explicit range; this is along the transverse axis + // minRange - () the minimum active range + // activeRange - () the currently active range + this.range = new Range(0,10000); + this.minRange = new Range(0,0); + this.activeRange = new Range(0,10000); +}; + +Trench.prototype = { + // ---------- + // Function: toString + // Prints [Trench edge type (parentItem)] for debug use + toString: function Trench_toString() { + return "[Trench " + this.edge + " " + this.type + + (this.parentItem ? " (" + this.parentItem + ")" : "") + + "]"; + }, + + //---------- + // Variable: radius + // (integer) radius is how far away we should snap from + get radius() this.customRadius || Trenches.defaultRadius, + + setParentItem: function Trench_setParentItem(item) { + if (!item.isAnItem) { + Utils.assert(false, "parentItem must be an Item"); + return false; + } + this.parentItem = item; + return true; + }, + + //---------- + // Function: setPosition + // set the trench's position. + // + // Parameters: + // position - (integer) px center position of the trench + // range - () the explicit active range of the trench + // minRange - () the minimum range of the trench + setPosition: function Trench_setPosition(position, range, minRange) { + this.position = position; + + var page = Items.getPageBounds(true); + + // optionally, set the range. + if (Utils.isRange(range)) { + this.range = range; + } else { + this.range = new Range(0, (this.xory == 'x' ? page.height : page.width)); + } + + // if there's a minRange, set that too. + if (Utils.isRange(minRange)) + this.minRange = minRange; + + // set the appropriate bounds as a rect. + if (this.xory == "x") // vertical + this.rect = new Rect(this.position - this.radius, this.range.min, 2 * this.radius, this.range.extent); + else // horizontal + this.rect = new Rect(this.range.min, this.position - this.radius, this.range.extent, 2 * this.radius); + + this.show(); // DEBUG + }, + + //---------- + // Function: setActiveRange + // set the trench's currently active range. + // + // Parameters: + // activeRange - () + setActiveRange: function Trench_setActiveRange(activeRange) { + if (!Utils.isRange(activeRange)) + return false; + this.activeRange = activeRange; + if (this.xory == "x") { // horizontal + this.activeRect = new Rect(this.position - this.radius, this.activeRange.min, 2 * this.radius, this.activeRange.extent); + this.guideRect = new Rect(this.position, this.activeRange.min, 0, this.activeRange.extent); + } else { // vertical + this.activeRect = new Rect(this.activeRange.min, this.position - this.radius, this.activeRange.extent, 2 * this.radius); + this.guideRect = new Rect(this.activeRange.min, this.position, this.activeRange.extent, 0); + } + return true; + }, + + //---------- + // Function: setWithRect + // Set the trench's position using the given rect. We know which side of the rect we should match + // because we've already recorded this information in . + // + // Parameters: + // rect - () + setWithRect: function Trench_setWithRect(rect) { + + if (!Utils.isRect(rect)) + Utils.error('argument must be Rect'); + + // First, calculate the range for this trench. + // Border trenches are always only active for the length of this range. + // Guide trenches, however, still use this value as its minRange. + if (this.xory == "x") + var range = new Range(rect.top - this.gutter, rect.bottom + this.gutter); + else + var range = new Range(rect.left - this.gutter, rect.right + this.gutter); + + if (this.type == "border") { + // border trenches have a range, so set that too. + if (this.edge == "left") + this.setPosition(rect.left - this.gutter, range); + else if (this.edge == "right") + this.setPosition(rect.right + this.gutter, range); + else if (this.edge == "top") + this.setPosition(rect.top - this.gutter, range); + else if (this.edge == "bottom") + this.setPosition(rect.bottom + this.gutter, range); + } else if (this.type == "guide") { + // guide trenches have no range, but do have a minRange. + if (this.edge == "left") + this.setPosition(rect.left, false, range); + else if (this.edge == "right") + this.setPosition(rect.right, false, range); + else if (this.edge == "top") + this.setPosition(rect.top, false, range); + else if (this.edge == "bottom") + this.setPosition(rect.bottom, false, range); + } + }, + + //---------- + // Function: show + // + // Show guide (dotted line), if is true. + // + // If is true, we will draw the trench. Active portions are drawn with 0.5 + // opacity. If is false, the entire trench will be + // very translucent. + show: function Trench_show() { // DEBUG + if (this.active && this.showGuide) { + if (!this.dom.guideTrench) + this.dom.guideTrench = iQ("
").addClass('guideTrench').css({id: 'guideTrench'+this.id}); + var guideTrench = this.dom.guideTrench; + guideTrench.css(this.guideRect); + iQ("body").append(guideTrench); + } else { + if (this.dom.guideTrench) { + this.dom.guideTrench.remove(); + delete this.dom.guideTrench; + } + } + + if (!Trenches.showDebug) { + this.hide(true); // true for dontHideGuides + return; + } + + if (!this.dom.visibleTrench) + this.dom.visibleTrench = iQ("
") + .addClass('visibleTrench') + .addClass(this.type) // border or guide + .css({id: 'visibleTrench'+this.id}); + var visibleTrench = this.dom.visibleTrench; + + if (!this.dom.activeVisibleTrench) + this.dom.activeVisibleTrench = iQ("
") + .addClass('activeVisibleTrench') + .addClass(this.type) // border or guide + .css({id: 'activeVisibleTrench'+this.id}); + var activeVisibleTrench = this.dom.activeVisibleTrench; + + if (this.active) + activeVisibleTrench.addClass('activeTrench'); + else + activeVisibleTrench.removeClass('activeTrench'); + + visibleTrench.css(this.rect); + activeVisibleTrench.css(this.activeRect || this.rect); + iQ("body").append(visibleTrench); + iQ("body").append(activeVisibleTrench); + }, + + //---------- + // Function: hide + // Hide the trench. + hide: function Trench_hide(dontHideGuides) { + if (this.dom.visibleTrench) + this.dom.visibleTrench.remove(); + if (this.dom.activeVisibleTrench) + this.dom.activeVisibleTrench.remove(); + if (!dontHideGuides && this.dom.guideTrench) + this.dom.guideTrench.remove(); + }, + + //---------- + // Function: rectOverlaps + // Given a , compute whether it overlaps with this trench. If it does, return an + // adjusted ("snapped") ; if it does not overlap, simply return false. + // + // Note that simply overlapping is not all that is required to be affected by this function. + // Trenches can only affect certain edges of rectangles... for example, a "left"-edge guide + // trench should only affect left edges of rectangles. We don't snap right edges to left-edged + // guide trenches. For border trenches, the logic is a bit different, so left snaps to right and + // top snaps to bottom. + // + // Parameters: + // rect - () the rectangle in question + // stationaryCorner - which corner is stationary? by default, the top left. + // "topleft", "bottomleft", "topright", "bottomright" + // assumeConstantSize - (boolean) whether the rect's dimensions are sacred or not + // keepProportional - (boolean) if we are allowed to change the rect's size, whether the + // dimensions should scaled proportionally or not. + // + // Returns: + // false - if rect does not overlap with this trench + // newRect - () an adjusted version of rect, if it is affected by this trench + rectOverlaps: function Trench_rectOverlaps(rect,stationaryCorner,assumeConstantSize,keepProportional) { + var edgeToCheck; + if (this.type == "border") { + if (this.edge == "left") + edgeToCheck = "right"; + else if (this.edge == "right") + edgeToCheck = "left"; + else if (this.edge == "top") + edgeToCheck = "bottom"; + else if (this.edge == "bottom") + edgeToCheck = "top"; + } else { // if trench type is guide or barrier... + edgeToCheck = this.edge; + } + + rect.adjustedEdge = edgeToCheck; + + switch (edgeToCheck) { + case "left": + if (this.ruleOverlaps(rect.left, rect.yRange)) { + if (stationaryCorner.indexOf('right') > -1) + rect.width = rect.right - this.position; + rect.left = this.position; + return rect; + } + break; + case "right": + if (this.ruleOverlaps(rect.right, rect.yRange)) { + if (assumeConstantSize) { + rect.left = this.position - rect.width; + } else { + var newWidth = this.position - rect.left; + if (keepProportional) + rect.height = rect.height * newWidth / rect.width; + rect.width = newWidth; + } + return rect; + } + break; + case "top": + if (this.ruleOverlaps(rect.top, rect.xRange)) { + if (stationaryCorner.indexOf('bottom') > -1) + rect.height = rect.bottom - this.position; + rect.top = this.position; + return rect; + } + break; + case "bottom": + if (this.ruleOverlaps(rect.bottom, rect.xRange)) { + if (assumeConstantSize) { + rect.top = this.position - rect.height; + } else { + var newHeight = this.position - rect.top; + if (keepProportional) + rect.width = rect.width * newHeight / rect.height; + rect.height = newHeight; + } + return rect; + } + } + + return false; + }, + + //---------- + // Function: ruleOverlaps + // Computes whether the given "rule" (a line segment, essentially), given by the position and + // range arguments, overlaps with the current trench. Note that this function assumes that + // the rule and the trench are in the same direction: both horizontal, or both vertical. + // + // Parameters: + // position - (integer) a position in px + // range - () the rule's range + ruleOverlaps: function Trench_ruleOverlaps(position, range) { + return (this.position - this.radius < position && + position < this.position + this.radius && + this.activeRange.overlaps(range)); + }, + + //---------- + // Function: adjustRangeIfIntercept + // Computes whether the given boundary (given as a position and its active range), perpendicular + // to the trench, intercepts the trench or not. If it does, it returns an adjusted for + // the trench. If not, it returns false. + // + // Parameters: + // position - (integer) the position of the boundary + // range - () the target's range, on the trench's transverse axis + adjustRangeIfIntercept: function Trench_adjustRangeIfIntercept(position, range) { + if (this.position - this.radius > range.min && this.position + this.radius < range.max) { + var activeRange = new Range(this.activeRange); + + // there are three ways this can go: + // 1. position < minRange.min + // 2. position > minRange.max + // 3. position >= minRange.min && position <= minRange.max + + if (position < this.minRange.min) { + activeRange.min = Math.min(this.minRange.min,position); + } else if (position > this.minRange.max) { + activeRange.max = Math.max(this.minRange.max,position); + } else { + // this should be impossible because items can't overlap and we've already checked + // that the range intercepts. + } + return activeRange; + } + return false; + }, + + //---------- + // Function: calculateActiveRange + // Computes and sets the for the trench, based on the around. + // This makes it so trenches' active ranges don't extend through other groupItems. + calculateActiveRange: function Trench_calculateActiveRange() { + + // set it to the default: just the range itself. + this.setActiveRange(this.range); + + // only guide-type trenches need to set a separate active range + if (this.type != 'guide') + return; + + var groupItems = GroupItems.groupItems; + var trench = this; + groupItems.forEach(function(groupItem) { + if (groupItem.isDragging) // floating groupItems don't block trenches + return; + if (trench.el == groupItem.container) // groupItems don't block their own trenches + return; + var bounds = groupItem.getBounds(); + var activeRange = new Range(); + if (trench.xory == 'y') { // if this trench is horizontal... + activeRange = trench.adjustRangeIfIntercept(bounds.left, bounds.yRange); + if (activeRange) + trench.setActiveRange(activeRange); + activeRange = trench.adjustRangeIfIntercept(bounds.right, bounds.yRange); + if (activeRange) + trench.setActiveRange(activeRange); + } else { // if this trench is vertical... + activeRange = trench.adjustRangeIfIntercept(bounds.top, bounds.xRange); + if (activeRange) + trench.setActiveRange(activeRange); + activeRange = trench.adjustRangeIfIntercept(bounds.bottom, bounds.xRange); + if (activeRange) + trench.setActiveRange(activeRange); + } + }); + } +}; + +// ########## +// Class: Trenches +// Singelton for managing all es. +var Trenches = { + // --------- + // Variables: + // nextId - (integer) a counter for the next 's value. + // showDebug - (boolean) whether to draw the es or not. + // defaultRadius - (integer) the default radius for new es. + // disabled - (boolean) whether trench-snapping is disabled or not. + nextId: 0, + showDebug: false, + defaultRadius: 10, + disabled: false, + + // --------- + // Variables: snapping preferences; used to break ties in snapping. + // preferTop - (boolean) prefer snapping to the top to the bottom + // preferLeft - (boolean) prefer snapping to the left to the right + preferTop: true, + get preferLeft() { return !UI.rtl; }, + + trenches: [], + + // ---------- + // Function: toString + // Prints [Trenches count=count] for debug use + toString: function Trenches_toString() { + return "[Trenches count=" + this.trenches.length + "]"; + }, + + // --------- + // Function: getById + // Return the specified . + // + // Parameters: + // id - (integer) + getById: function Trenches_getById(id) { + return this.trenches[id]; + }, + + // --------- + // Function: register + // Register a new and returns the resulting ID. + // + // Parameters: + // See the constructor 's parameters. + // + // Returns: + // id - (int) the new 's ID. + register: function Trenches_register(element, xory, type, edge) { + var trench = new Trench(element, xory, type, edge); + this.trenches[trench.id] = trench; + return trench.id; + }, + + // --------- + // Function: registerWithItem + // Register a whole set of es using an and returns the resulting IDs. + // + // Parameters: + // item - the to project trenches + // type - either "border" or "guide" + // + // Returns: + // ids - array of the new es' IDs. + registerWithItem: function Trenches_registerWithItem(item, type) { + var container = item.container; + var ids = {}; + ids.left = Trenches.register(container,"x",type,"left"); + ids.right = Trenches.register(container,"x",type,"right"); + ids.top = Trenches.register(container,"y",type,"top"); + ids.bottom = Trenches.register(container,"y",type,"bottom"); + + this.getById(ids.left).setParentItem(item); + this.getById(ids.right).setParentItem(item); + this.getById(ids.top).setParentItem(item); + this.getById(ids.bottom).setParentItem(item); + + return ids; + }, + + // --------- + // Function: unregister + // Unregister one or more es. + // + // Parameters: + // ids - (integer) a single ID or (array) a list of IDs. + unregister: function Trenches_unregister(ids) { + if (!Array.isArray(ids)) + ids = [ids]; + var self = this; + ids.forEach(function(id) { + self.trenches[id].hide(); + delete self.trenches[id]; + }); + }, + + // --------- + // Function: activateOthersTrenches + // Activate all es other than those projected by the current element. + // + // Parameters: + // element - (DOMElement) the DOM element of the Item being dragged or resized. + activateOthersTrenches: function Trenches_activateOthersTrenches(element) { + this.trenches.forEach(function(t) { + if (t.el === element) + return; + if (t.parentItem && (t.parentItem.isAFauxItem || t.parentItem.isDragging)) + return; + t.active = true; + t.calculateActiveRange(); + t.show(); // debug + }); + }, + + // --------- + // Function: disactivate + // After , disactivates all the es again. + disactivate: function Trenches_disactivate() { + this.trenches.forEach(function(t) { + t.active = false; + t.showGuide = false; + t.show(); + }); + }, + + // --------- + // Function: hideGuides + // Hide all guides (dotted lines) en masse. + hideGuides: function Trenches_hideGuides() { + this.trenches.forEach(function(t) { + t.showGuide = false; + t.show(); + }); + }, + + // --------- + // Function: snap + // Used to "snap" an object's bounds to active trenches and to the edge of the window. + // If the meta key is down (), it will not snap but will still enforce the rect + // not leaving the safe bounds of the window. + // + // Parameters: + // rect - () the object's current bounds + // stationaryCorner - which corner is stationary? by default, the top left. + // "topleft", "bottomleft", "topright", "bottomright" + // assumeConstantSize - (boolean) whether the rect's dimensions are sacred or not + // keepProportional - (boolean) if we are allowed to change the rect's size, whether the + // dimensions should scaled proportionally or not. + // + // Returns: + // () - the updated bounds, if they were updated + // false - if the bounds were not updated + snap: function Trenches_snap(rect,stationaryCorner,assumeConstantSize,keepProportional) { + // hide all the guide trenches, because the correct ones will be turned on later. + Trenches.hideGuides(); + + var updated = false; + var updatedX = false; + var updatedY = false; + + var snappedTrenches = {}; + + for (var i in this.trenches) { + var t = this.trenches[i]; + if (!t.active) + continue; + // newRect will be a new rect, or false + var newRect = t.rectOverlaps(rect,stationaryCorner,assumeConstantSize,keepProportional); + + if (newRect) { // if rectOverlaps returned an updated rect... + + if (assumeConstantSize && updatedX && updatedY) + break; + if (assumeConstantSize && updatedX && (newRect.adjustedEdge == "left"||newRect.adjustedEdge == "right")) + continue; + if (assumeConstantSize && updatedY && (newRect.adjustedEdge == "top"||newRect.adjustedEdge == "bottom")) + continue; + + rect = newRect; + updated = true; + + // register this trench as the "snapped trench" for the appropriate edge. + snappedTrenches[newRect.adjustedEdge] = t; + + // if updatedX, we don't need to update x any more. + if (newRect.adjustedEdge == "left" && this.preferLeft) + updatedX = true; + if (newRect.adjustedEdge == "right" && !this.preferLeft) + updatedX = true; + + // if updatedY, we don't need to update x any more. + if (newRect.adjustedEdge == "top" && this.preferTop) + updatedY = true; + if (newRect.adjustedEdge == "bottom" && !this.preferTop) + updatedY = true; + + } + } + + if (updated) { + rect.snappedTrenches = snappedTrenches; + return rect; + } + return false; + }, + + // --------- + // Function: show + // all es. + show: function Trenches_show() { + this.trenches.forEach(function(t) { + t.show(); + }); + }, + + // --------- + // Function: toggleShown + // Toggle and trigger + toggleShown: function Trenches_toggleShown() { + this.showDebug = !this.showDebug; + this.show(); + } +};