browser/components/customizableui/src/DragPositionManager.jsm

Wed, 31 Dec 2014 13:27:57 +0100

author
Michael Schloh von Bennewitz <michael@schloh.com>
date
Wed, 31 Dec 2014 13:27:57 +0100
branch
TOR_BUG_3246
changeset 6
8bccb770b82d
permissions
-rw-r--r--

Ignore runtime configuration files generated during quality assurance.

     1 /* This Source Code Form is subject to the terms of the Mozilla Public
     2  * License, v. 2.0. If a copy of the MPL was not distributed with this
     3  * file, You can obtain one at http://mozilla.org/MPL/2.0/. */
     5 "use strict";
     7 Components.utils.import("resource:///modules/CustomizableUI.jsm");
     9 let gManagers = new WeakMap();
    11 const kPaletteId = "customization-palette";
    12 const kPlaceholderClass = "panel-customization-placeholder";
    14 this.EXPORTED_SYMBOLS = ["DragPositionManager"];
    16 function AreaPositionManager(aContainer) {
    17   // Caching the direction and bounds of the container for quick access later:
    18   let window = aContainer.ownerDocument.defaultView;
    19   this._dir = window.getComputedStyle(aContainer).direction;
    20   let containerRect = aContainer.getBoundingClientRect();
    21   this._containerInfo = {
    22     left: containerRect.left,
    23     right: containerRect.right,
    24     top: containerRect.top,
    25     width: containerRect.width
    26   };
    27   this._inPanel = aContainer.id == CustomizableUI.AREA_PANEL;
    28   this._horizontalDistance = null;
    29   this.update(aContainer);
    30 }
    32 AreaPositionManager.prototype = {
    33   _nodePositionStore: null,
    34   _wideCache: null,
    36   update: function(aContainer) {
    37     let window = aContainer.ownerDocument.defaultView;
    38     this._nodePositionStore = new WeakMap();
    39     this._wideCache = new Set();
    40     let last = null;
    41     let singleItemHeight;
    42     for (let child of aContainer.children) {
    43       if (child.hidden) {
    44         continue;
    45       }
    46       let isNodeWide = this._checkIfWide(child);
    47       if (isNodeWide) {
    48         this._wideCache.add(child.id);
    49       }
    50       let coordinates = this._lazyStoreGet(child);
    51       // We keep a baseline horizontal distance between non-wide nodes around
    52       // for use when we can't compare with previous/next nodes
    53       if (!this._horizontalDistance && last && !isNodeWide) {
    54         this._horizontalDistance = coordinates.left - last.left;
    55       }
    56       // We also keep the basic height of non-wide items for use below:
    57       if (!isNodeWide && !singleItemHeight) {
    58         singleItemHeight = coordinates.height;
    59       }
    60       last = !isNodeWide ? coordinates : null;
    61     }
    62     if (this._inPanel) {
    63       this._heightToWidthFactor = CustomizableUI.PANEL_COLUMN_COUNT;
    64     } else {
    65       this._heightToWidthFactor = this._containerInfo.width / singleItemHeight;
    66     }
    67   },
    69   /**
    70    * Find the closest node in the container given the coordinates.
    71    * "Closest" is defined in a somewhat strange manner: we prefer nodes
    72    * which are in the same row over nodes that are in a different row.
    73    * In order to implement this, we use a weighted cartesian distance
    74    * where dy is more heavily weighted by a factor corresponding to the
    75    * ratio between the container's width and the height of its elements.
    76    */
    77   find: function(aContainer, aX, aY, aDraggedItemId) {
    78     let closest = null;
    79     let minCartesian = Number.MAX_VALUE;
    80     let containerX = this._containerInfo.left;
    81     let containerY = this._containerInfo.top;
    82     for (let node of aContainer.children) {
    83       let coordinates = this._lazyStoreGet(node);
    84       let offsetX = coordinates.x - containerX;
    85       let offsetY = coordinates.y - containerY;
    86       let hDiff = offsetX - aX;
    87       let vDiff = offsetY - aY;
    88       // For wide widgets, we're always going to be further from the center
    89       // horizontally. Compensate:
    90       if (this.isWide(node)) {
    91         hDiff /= CustomizableUI.PANEL_COLUMN_COUNT;
    92       }
    93       // Then compensate for the height/width ratio so that we prefer items
    94       // which are in the same row:
    95       hDiff /= this._heightToWidthFactor;
    97       let cartesianDiff = hDiff * hDiff + vDiff * vDiff;
    98       if (cartesianDiff < minCartesian) {
    99         minCartesian = cartesianDiff;
   100         closest = node;
   101       }
   102     }
   104     // Now correct this node based on what we're dragging
   105     if (closest) {
   106       let doc = aContainer.ownerDocument;
   107       let draggedItem = doc.getElementById(aDraggedItemId);
   108       // If dragging a wide item, always pick the first item in a row:
   109       if (this._inPanel && draggedItem &&
   110           draggedItem.classList.contains(CustomizableUI.WIDE_PANEL_CLASS)) {
   111         return this._firstInRow(closest);
   112       }
   113       let targetBounds = this._lazyStoreGet(closest);
   114       let farSide = this._dir == "ltr" ? "right" : "left";
   115       let outsideX = targetBounds[farSide];
   116       // Check if we're closer to the next target than to this one:
   117       // Only move if we're not targeting a node in a different row:
   118       if (aY > targetBounds.top && aY < targetBounds.bottom) {
   119         if ((this._dir == "ltr" && aX > outsideX) ||
   120             (this._dir == "rtl" && aX < outsideX)) {
   121           return closest.nextSibling || aContainer;
   122         }
   123       }
   124     }
   125     return closest;
   126   },
   128   /**
   129    * "Insert" a "placeholder" by shifting the subsequent children out of the
   130    * way. We go through all the children, and shift them based on the position
   131    * they would have if we had inserted something before aBefore. We use CSS
   132    * transforms for this, which are CSS transitioned.
   133    */
   134   insertPlaceholder: function(aContainer, aBefore, aWide, aSize, aIsFromThisArea) {
   135     let isShifted = false;
   136     let shiftDown = aWide;
   137     for (let child of aContainer.children) {
   138       // Don't need to shift hidden nodes:
   139       if (child.getAttribute("hidden") == "true") {
   140         continue;
   141       }
   142       // If this is the node before which we're inserting, start shifting
   143       // everything that comes after. One exception is inserting at the end
   144       // of the menupanel, in which case we do not shift the placeholders:
   145       if (child == aBefore && !child.classList.contains(kPlaceholderClass)) {
   146         isShifted = true;
   147         // If the node before which we're inserting is wide, we should
   148         // shift everything one row down:
   149         if (!shiftDown && this.isWide(child)) {
   150           shiftDown = true;
   151         }
   152       }
   153       // If we're moving items before a wide node that were already there,
   154       // it's possible it's not necessary to shift nodes
   155       // including & after the wide node.
   156       if (this.__undoShift) {
   157         isShifted = false;
   158       }
   159       if (isShifted) {
   160         // Conversely, if we're adding something before a wide node, for
   161         // simplicity's sake we move everything including the wide node down:
   162         if (this.__moveDown) {
   163           shiftDown = true;
   164         }
   165         if (aIsFromThisArea && !this._lastPlaceholderInsertion) {
   166           child.setAttribute("notransition", "true");
   167         }
   168         // Determine the CSS transform based on the next node:
   169         child.style.transform = this._getNextPos(child, shiftDown, aSize);
   170       } else {
   171         // If we're not shifting this node, reset the transform
   172         child.style.transform = "";
   173       }
   174     }
   175     if (aContainer.lastChild && aIsFromThisArea &&
   176         !this._lastPlaceholderInsertion) {
   177       // Flush layout:
   178       aContainer.lastChild.getBoundingClientRect();
   179       // then remove all the [notransition]
   180       for (let child of aContainer.children) {
   181         child.removeAttribute("notransition");
   182       }
   183     }
   184     delete this.__moveDown;
   185     delete this.__undoShift;
   186     this._lastPlaceholderInsertion = aBefore;
   187   },
   189   isWide: function(aNode) {
   190     return this._wideCache.has(aNode.id);
   191   },
   193   _checkIfWide: function(aNode) {
   194     return this._inPanel && aNode && aNode.firstChild &&
   195            aNode.firstChild.classList.contains(CustomizableUI.WIDE_PANEL_CLASS);
   196   },
   198   /**
   199    * Reset all the transforms in this container, optionally without
   200    * transitioning them.
   201    * @param aContainer    the container in which to reset transforms
   202    * @param aNoTransition if truthy, adds a notransition attribute to the node
   203    *                      while resetting the transform.
   204    */
   205   clearPlaceholders: function(aContainer, aNoTransition) {
   206     for (let child of aContainer.children) {
   207       if (aNoTransition) {
   208         child.setAttribute("notransition", true);
   209       }
   210       child.style.transform = "";
   211       if (aNoTransition) {
   212         // Need to force a reflow otherwise this won't work.
   213         child.getBoundingClientRect();
   214         child.removeAttribute("notransition");
   215       }
   216     }
   217     // We snapped back, so we can assume there's no more
   218     // "last" placeholder insertion point to keep track of.
   219     if (aNoTransition) {
   220       this._lastPlaceholderInsertion = null;
   221     }
   222   },
   224   _getNextPos: function(aNode, aShiftDown, aSize) {
   225     // Shifting down is easy:
   226     if (this._inPanel && aShiftDown) {
   227       return "translate(0, " + aSize.height + "px)";
   228     }
   229     return this._diffWithNext(aNode, aSize);
   230   },
   232   _diffWithNext: function(aNode, aSize) {
   233     let xDiff;
   234     let yDiff = null;
   235     let nodeBounds = this._lazyStoreGet(aNode);
   236     let side = this._dir == "ltr" ? "left" : "right";
   237     let next = this._getVisibleSiblingForDirection(aNode, "next");
   238     // First we determine the transform along the x axis.
   239     // Usually, there will be a next node to base this on:
   240     if (next) {
   241       let otherBounds = this._lazyStoreGet(next);
   242       xDiff = otherBounds[side] - nodeBounds[side];
   243       // If the next node is a wide item in the panel, check if we could maybe
   244       // just move further out in the same row, without snapping to the next
   245       // one. This happens, for example, if moving an item that's before a wide
   246       // node within its own row of items. There will be space to drop this
   247       // item within the row, and the rest of the items do not need to shift.
   248       if (this.isWide(next)) {
   249         let otherXDiff = this._moveNextBasedOnPrevious(aNode, nodeBounds,
   250                                                        this._firstInRow(aNode));
   251         // If this has the same sign as our original shift, we're still
   252         // snapping to the start of the row. In this case, we should move
   253         // everything after us a row down, so as not to display two nodes on
   254         // top of each other:
   255         // (we would be able to get away with checking for equality instead of
   256         //  equal signs here, but one of these is based on the x coordinate of
   257         //  the first item in row N and one on that for row N - 1, so this is
   258         //  safer, as their margins might differ)
   259         if ((otherXDiff < 0) == (xDiff < 0)) {
   260           this.__moveDown = true;
   261         } else {
   262           // Otherwise, we succeeded and can move further out. This also means
   263           // we can stop shifting the rest of the content:
   264           xDiff = otherXDiff;
   265           this.__undoShift = true;
   266         }
   267       } else {
   268         // We set this explicitly because otherwise some strange difference
   269         // between the height and the actual difference between line creeps in
   270         // and messes with alignments
   271         yDiff = otherBounds.top - nodeBounds.top;
   272       }
   273     } else {
   274       // We don't have a sibling whose position we can use. First, let's see
   275       // if we're also the first item (which complicates things):
   276       let firstNode = this._firstInRow(aNode);
   277       if (aNode == firstNode) {
   278         // Maybe we stored the horizontal distance between non-wide nodes,
   279         // if not, we'll use the width of the incoming node as a proxy:
   280         xDiff = this._horizontalDistance || aSize.width;
   281       } else {
   282         // If not, we should be able to get the distance to the previous node
   283         // and use the inverse, unless there's no room for another node (ie we
   284         // are the last node and there's no room for another one)
   285         xDiff = this._moveNextBasedOnPrevious(aNode, nodeBounds, firstNode);
   286       }
   287     }
   289     // If we've not determined the vertical difference yet, check it here
   290     if (yDiff === null) {
   291       // If the next node is behind rather than in front, we must have moved
   292       // vertically:
   293       if ((xDiff > 0 && this._dir == "rtl") || (xDiff < 0 && this._dir == "ltr")) {
   294         yDiff = aSize.height;
   295       } else {
   296         // Otherwise, we haven't
   297         yDiff = 0;
   298       }
   299     }
   300     return "translate(" + xDiff + "px, " + yDiff + "px)";
   301   },
   303   /**
   304    * Helper function to find the transform a node if there isn't a next node
   305    * to base that on.
   306    * @param aNode           the node to transform
   307    * @param aNodeBounds     the bounding rect info of this node
   308    * @param aFirstNodeInRow the first node in aNode's row
   309    */
   310   _moveNextBasedOnPrevious: function(aNode, aNodeBounds, aFirstNodeInRow) {
   311     let next = this._getVisibleSiblingForDirection(aNode, "previous");
   312     let otherBounds = this._lazyStoreGet(next);
   313     let side = this._dir == "ltr" ? "left" : "right";
   314     let xDiff = aNodeBounds[side] - otherBounds[side];
   315     // If, however, this means we move outside the container's box
   316     // (i.e. the row in which this item is placed is full)
   317     // we should move it to align with the first item in the next row instead
   318     let bound = this._containerInfo[this._dir == "ltr" ? "right" : "left"];
   319     if ((this._dir == "ltr" && xDiff + aNodeBounds.right > bound) ||
   320         (this._dir == "rtl" && xDiff + aNodeBounds.left < bound)) {
   321       xDiff = this._lazyStoreGet(aFirstNodeInRow)[side] - aNodeBounds[side];
   322     }
   323     return xDiff;
   324   },
   326   /**
   327    * Get position details from our cache. If the node is not yet cached, get its position
   328    * information and cache it now.
   329    * @param aNode  the node whose position info we want
   330    * @return the position info
   331    */
   332   _lazyStoreGet: function(aNode) {
   333     let rect = this._nodePositionStore.get(aNode);
   334     if (!rect) {
   335       // getBoundingClientRect() returns a DOMRect that is live, meaning that
   336       // as the element moves around, the rects values change. We don't want
   337       // that - we want a snapshot of what the rect values are right at this
   338       // moment, and nothing else. So we have to clone the values.
   339       let clientRect = aNode.getBoundingClientRect();
   340       rect = {
   341         left: clientRect.left,
   342         right: clientRect.right,
   343         width: clientRect.width,
   344         height: clientRect.height,
   345         top: clientRect.top,
   346         bottom: clientRect.bottom,
   347       };
   348       rect.x = rect.left + rect.width / 2;
   349       rect.y = rect.top + rect.height / 2;
   350       Object.freeze(rect);
   351       this._nodePositionStore.set(aNode, rect);
   352     }
   353     return rect;
   354   },
   356   _firstInRow: function(aNode) {
   357     // XXXmconley: I'm not entirely sure why we need to take the floor of these
   358     // values - it looks like, periodically, we're getting fractional pixels back
   359     //from lazyStoreGet. I've filed bug 994247 to investigate.
   360     let bound = Math.floor(this._lazyStoreGet(aNode).top);
   361     let rv = aNode;
   362     let prev;
   363     while (rv && (prev = this._getVisibleSiblingForDirection(rv, "previous"))) {
   364       if (Math.floor(this._lazyStoreGet(prev).bottom) <= bound) {
   365         return rv;
   366       }
   367       rv = prev;
   368     }
   369     return rv;
   370   },
   372   _getVisibleSiblingForDirection: function(aNode, aDirection) {
   373     let rv = aNode;
   374     do {
   375       rv = rv[aDirection + "Sibling"];
   376     } while (rv && rv.getAttribute("hidden") == "true")
   377     return rv;
   378   }
   379 }
   381 let DragPositionManager = {
   382   start: function(aWindow) {
   383     let areas = CustomizableUI.areas.filter((area) => CustomizableUI.getAreaType(area) != "toolbar");
   384     areas = areas.map((area) => CustomizableUI.getCustomizeTargetForArea(area, aWindow));
   385     areas.push(aWindow.document.getElementById(kPaletteId));
   386     for (let areaNode of areas) {
   387       let positionManager = gManagers.get(areaNode);
   388       if (positionManager) {
   389         positionManager.update(areaNode);
   390       } else {
   391         gManagers.set(areaNode, new AreaPositionManager(areaNode));
   392       }
   393     }
   394   },
   396   add: function(aWindow, aArea, aContainer) {
   397     if (CustomizableUI.getAreaType(aArea) != "toolbar") {
   398       return;
   399     }
   401     gManagers.set(aContainer, new AreaPositionManager(aContainer));
   402   },
   404   remove: function(aWindow, aArea, aContainer) {
   405     if (CustomizableUI.getAreaType(aArea) != "toolbar") {
   406       return;
   407     }
   409     gManagers.delete(aContainer);
   410   },
   412   stop: function() {
   413     gManagers.clear();
   414   },
   416   getManagerForArea: function(aArea) {
   417     return gManagers.get(aArea);
   418   }
   419 };
   421 Object.freeze(DragPositionManager);

mercurial