Index: chrome/browser/resources/options/chromeos/display_layout_manager_multi.js |
diff --git a/chrome/browser/resources/options/chromeos/display_layout_manager_multi.js b/chrome/browser/resources/options/chromeos/display_layout_manager_multi.js |
new file mode 100644 |
index 0000000000000000000000000000000000000000..34d3786aebf7167819eb3a4ff05f33ec99773fb1 |
--- /dev/null |
+++ b/chrome/browser/resources/options/chromeos/display_layout_manager_multi.js |
@@ -0,0 +1,404 @@ |
+/** |
+ * @fileoverview Description of this file. |
+ */ |
+// Copyright 2016 The Chromium Authors. All rights reserved. |
+// Use of this source code is governed by a BSD-style license that can be |
+// found in the LICENSE file. |
+ |
+cr.exportPath('options'); |
+ |
+cr.define('options', function() { |
+ 'use strict'; |
+ |
+ var DisplayLayoutManager = options.DisplayLayoutManager; |
+ |
+ var sIdPrefix = 'fakeId'; |
+ |
+ var gFakeDisplayNum = 3; |
+ var gFakeDisplays = []; |
+ |
+ /** |
+ * @constructor |
+ * @extends {options.DisplayLayoutManager} |
+ */ |
+ function DisplayLayoutManagerMulti() { |
+ DisplayLayoutManager.call(this); |
+ } |
+ |
+ // Helper class for display layout management. Implements logic for laying |
+ // out 3+ displays. |
+ DisplayLayoutManagerMulti.prototype = { |
+ __proto__: DisplayLayoutManager.prototype, |
+ |
+ /** @type {string} */ |
+ dragId_: '', |
+ |
+ /** @type {string} */ |
+ dragParentId_: '', |
+ |
+ /** @type {options.DisplayLayoutType} */ |
+ dragLayoutType_: options.DisplayLayoutType.RIGHT, |
+ |
+ /** @override */ |
+ createDisplayArea: function(displayAreaDiv, minVisualScale) { |
+ // Add fake displays just before creating divs. |
+ this.createFakeDisplays_(gFakeDisplayNum); |
+ for (var i = 0; i < gFakeDisplayNum; ++i) { |
+ var fake = gFakeDisplays[i]; |
+ this.displayLayoutMap_[fake.id] = fake; |
+ } |
+ return DisplayLayoutManager.prototype.createDisplayArea.call( |
+ this, displayAreaDiv, minVisualScale); |
+ }, |
+ |
+ /** @override */ |
+ setFocusedId: function(focusedId) { |
+ DisplayLayoutManager.prototype.setFocusedId.call(this, focusedId); |
+ var layout = this.displayLayoutMap_[focusedId]; |
+ this.highlightParentEdge_(layout.parentId, layout.layoutType); |
+ }, |
+ |
+ /** @override */ |
+ updatePosition: function(id, newPosition) { |
+ this.dragId_ = id; |
+ var layout = this.displayLayoutMap_[id]; |
+ this.dragParentId_ = this.findClosest_(layout, newPosition); |
+ var parent = this.displayLayoutMap_[this.dragParentId_]; |
+ this.dragLayoutType_ = |
+ this.getLayoutTypeForPosition_(layout.div, parent.div, newPosition); |
+ this.updateDivPosition_( |
+ layout.div, newPosition, parent.div, this.dragLayoutType_); |
+ this.highlightParentEdge_(this.dragParentId_, this.dragLayoutType_); |
+ }, |
+ |
+ /** @override */ |
+ finalizePosition: function(id) { |
+ if (id != this.dragId_) |
+ return false; |
+ |
+ var layout = this.displayLayoutMap_[id]; |
+ var parent = this.displayLayoutMap_[this.dragParentId_]; |
+ |
+ // All immediate children of |layout| need to be re-parented. |
+ var orphans = this.findChildren_(id, false /* do not recurse */); |
+ |
+ // Special case: re-parenting to a descendant. Parent the immediate child |
+ // to the dragged display's parent and treat that as the moved layout. |
+ var newParent = parent; |
+ while (newParent) { |
+ if (newParent.parentId == '') |
+ break; |
+ if (newParent.parentId == id) { |
+ newParent.parentId = layout.parentId; |
+ break; |
+ } |
+ newParent = this.displayLayoutMap_[newParent.parentId]; |
+ } |
+ |
+ // Re-parent the dragged display. |
+ layout.parentId = this.dragParentId_; |
+ layout.layoutType = this.dragLayoutType_; |
+ |
+ // Snap to corners and calculate the offset from the new parent. This does |
+ // not depend on any unresolved child layout. |
+ this.adjustCorners_(layout.div, parent.div, this.dragLayoutType_); |
+ this.calculateOffset_(layout); |
+ |
+ // Update any orphaned children, which may include |id|. |
+ this.updateOrphans_(orphans); |
+ |
+ // Update the fake displays. |
+ for (var i = 0; i < gFakeDisplayNum; ++i) { |
+ var fakeId = gFakeDisplays[i].id; |
+ var fake = Object.assign({}, this.displayLayoutMap_[fakeId]); |
+ gFakeDisplays[i] = fake; |
+ fake.bounds = this.calcLayoutBounds_(fake); |
+ } |
+ |
+ return layout.originalDivOffsets.x != layout.div.offsetLeft || |
+ layout.originalDivOffsets.y != layout.div.offsetTop; |
+ }, |
+ |
+ /** |
+ * @param {Array<string>=} orphanedIds The list of ids affected by the move. |
+ * @private |
+ */ |
+ updateOrphans_(orphanedIds) { |
+ var orphans = orphanedIds.slice(); |
+ for (var orphan of orphanedIds) { |
+ var newOrphans = this.findChildren_(orphan, true /* recurse */); |
+ // If the dragged display was re-parented to one of its children, |
+ // there may be duplicates so merge the lists. |
+ for (var o of newOrphans) { |
+ if (!orphans.includes(o)) |
+ orphans.push(o); |
+ } |
+ } |
+ |
+ // Re-parent each orphan to a layout that is not a member of |orphans|. |
+ // We remove each orphan from the list so that subsequent orphans can be |
+ // parented to initial (primary) orphans. |
+ while (orphans.length) { |
+ var childId = orphans.shift(); |
+ var child = this.displayLayoutMap_[childId]; |
+ |
+ if (childId == this.dragId_) { |
+ // Preserve the layout and offset of the dragged div. |
+ this.calcLayoutBounds_(child); |
+ this.layoutDivFromBounds_(child); |
+ continue; |
+ } |
+ |
+ var pos = {x: child.div.offsetLeft, y: child.div.offsetTop}; |
+ var newParentId = this.findClosest_(child, pos, orphans); |
+ child.parentId = newParentId; |
+ // If all displays are orphans, newParentId will be empty. |
+ if (newParentId != '') { |
+ var parent = this.displayLayoutMap_[newParentId]; |
+ var layoutType = |
+ this.getLayoutTypeForPosition_(child.div, parent.div, pos); |
+ this.updateDivPosition_(child.div, pos, parent.div, layoutType); |
+ this.adjustCorners_(child.div, parent.div, child.layoutType); |
+ this.calculateOffset_(child); |
+ } |
+ // TODO(stevenjb): Set bounds and send child update. |
+ } |
+ }, |
+ |
+ /** |
+ * @param {string} parentId |
+ * @param {boolean} recurse Include descendants of children. |
+ * @return {!Array<string>} |
+ * @private |
+ */ |
+ findChildren_(parentId, recurse) { |
+ var children = []; |
+ for (var childId in this.displayLayoutMap_) { |
+ if (childId == parentId) |
+ continue; |
+ if (this.displayLayoutMap_[childId].parentId == parentId) { |
+ // Insert immediate children at the front of the array. |
+ children.unshift(childId); |
+ if (recurse) { |
+ // Descendants get added to the end of the list. |
+ children = children.concat(this.findChildren_(childId, true)); |
+ } |
+ } |
+ } |
+ return children; |
+ }, |
+ |
+ /** |
+ * Finds the display closest to |position| ignoring |child|. |
+ * @param {options.DisplayLayout} child |
+ * @param {options.DisplayPosition} position |
+ * @param {Array<string>=} opt_ignore Ids to ignore. |
+ * @return {string} |
+ * @private |
+ */ |
+ findClosest_: function(child, position, opt_ignore) { |
+ var closestDist = undefined; |
+ var x = position.x + child.div.offsetWidth / 2; |
+ var y = position.y + child.div.offsetHeight / 2; |
+ var closestId = '', closestDelta2; |
+ for (var id in this.displayLayoutMap_) { |
+ if (id == child.id) |
+ continue; |
+ if (opt_ignore && opt_ignore.includes(id)) |
+ continue; |
+ var div = this.displayLayoutMap_[id].div; |
+ var left = div.offsetLeft; |
+ var top = div.offsetTop; |
+ var width = div.offsetWidth; |
+ var height = div.offsetHeight; |
+ if (x >= left && x < left + width && y >= top && y < top + height) |
+ return id; // point is inside rect |
+ var dx, dy; |
+ if (x < left) |
+ dx = left - x; |
+ else if (x > left + width) |
+ dx = x - (left + width); |
+ else |
+ dx = 0; |
+ if (y < top) |
+ dy = top - y; |
+ else if (y > top + height) |
+ dy = y - (top + height); |
+ else |
+ dy = 0; |
+ var delta2 = dx * dx + dy * dy; |
+ if (closestId == '' || delta2 < closestDelta2) { |
+ closestId = id; |
+ closestDelta2 = delta2; |
+ } |
+ } |
+ return closestId; |
+ }, |
+ |
+ /** |
+ * Calculates the layoutType for |position| relative to |parentDiv|. |
+ * @param {?HTMLElement} div |
+ * @param {?HTMLElement} parentDiv |
+ * @param {options.DisplayPosition} position |
+ * @return {options.DisplayLayoutType} |
+ * @private |
+ */ |
+ getLayoutTypeForPosition_: function(div, parentDiv, position) { |
+ // Translate position from top-left to center. |
+ var x = position.x + div.offsetWidth / 2; |
+ var y = position.y + div.offsetHeight / 2; |
+ |
+ // Determine the distance from the new position to both of the near edges. |
+ var div = parentDiv; |
+ var left = div.offsetLeft; |
+ var top = div.offsetTop; |
+ var width = div.offsetWidth; |
+ var height = div.offsetHeight; |
+ // Signed deltas to center. |
+ var dx = x - (left + width / 2); |
+ var dy = y - (top + height / 2); |
+ // Unsigned distance to each edge. |
+ var distx = Math.abs(dx) - width / 2; |
+ var disty = Math.abs(dy) - height / 2; |
+ if (distx > disty) { |
+ if (dx < 0) |
+ return options.DisplayLayoutType.LEFT; |
+ else |
+ return options.DisplayLayoutType.RIGHT; |
+ } else { |
+ if (dy < 0) |
+ return options.DisplayLayoutType.TOP; |
+ else |
+ return options.DisplayLayoutType.BOTTOM; |
+ } |
+ }, |
+ |
+ /** |
+ * Update the location |div| to the position closest to |newPosition| along |
+ * the edge of |parentDiv| specified by |layoutType|. |
+ * @param {?HTMLElement} div |
+ * @param {options.DisplayPosition} newPosition |
+ * @param {?HTMLElement} parentDiv |
+ * @param {!options.DisplayLayoutType} layoutType |
+ * @private |
+ */ |
+ updateDivPosition_(div, newPosition, parentDiv, layoutType) { |
+ var snapX = (layoutType == options.DisplayLayoutType.LEFT || |
+ layoutType == options.DisplayLayoutType.RIGHT) ? |
+ 0 /* infinite */ : |
+ undefined /* default */; |
+ var snapY = (layoutType == options.DisplayLayoutType.TOP || |
+ layoutType == options.DisplayLayoutType.BOTTOM) ? |
+ 0 /* infinite */ : |
+ undefined /* default */; |
+ newPosition.x = this.snapToEdge_( |
+ newPosition.x, div.offsetWidth, parentDiv.offsetLeft, |
+ parentDiv.offsetWidth, snapX); |
+ newPosition.y = this.snapToEdge_( |
+ newPosition.y, div.offsetHeight, parentDiv.offsetTop, |
+ parentDiv.offsetHeight, snapY); |
+ |
+ this.setDivPosition_(div, newPosition, parentDiv, layoutType); |
+ }, |
+ |
+ /** |
+ * Highlights the edge of the div associated with |parentId| based on |
+ * |layoutType|. |
+ * @param {string} parentId |
+ * @param {options.DisplayLayoutType} layoutType |
+ * @private |
+ */ |
+ highlightParentEdge_(parentId, layoutType) { |
+ for (var tid in this.displayLayoutMap_) { |
+ var tlayout = this.displayLayoutMap_[tid]; |
+ var highlight = ''; |
+ if (tlayout.id == parentId) { |
+ switch (layoutType) { |
+ case options.DisplayLayoutType.RIGHT: |
+ highlight = 'displays-parent-right'; |
+ break; |
+ case options.DisplayLayoutType.LEFT: |
+ highlight = 'displays-parent-left'; |
+ break; |
+ case options.DisplayLayoutType.TOP: |
+ highlight = 'displays-parent-top'; |
+ break; |
+ case options.DisplayLayoutType.BOTTOM: |
+ highlight = 'displays-parent-bottom'; |
+ break; |
+ } |
+ } |
+ tlayout.div.classList.toggle( |
+ 'displays-parent-right', highlight == 'displays-parent-right'); |
+ tlayout.div.classList.toggle( |
+ 'displays-parent-left', highlight == 'displays-parent-left'); |
+ tlayout.div.classList.toggle( |
+ 'displays-parent-top', highlight == 'displays-parent-top'); |
+ tlayout.div.classList.toggle( |
+ 'displays-parent-bottom', highlight == 'displays-parent-bottom'); |
+ } |
+ }, |
+ |
+ createFakeDisplays_: function(num) { |
+ if (num != gFakeDisplayNum) |
+ gFakeDisplays = []; |
+ var primary; |
+ for (var id in this.displayLayoutMap_) { |
+ var layout = this.displayLayoutMap_[id]; |
+ if (layout.parentId == '') |
+ primary = layout; |
+ } |
+ for (var i = 0; i < num; ++i) { |
+ if (i < gFakeDisplays.length) { |
+ gFakeDisplays[i].div = null; |
+ } else { |
+ var fakeId = sIdPrefix + i; |
+ var layoutType = /** @type {options.DisplayLayoutType} */ (i % 4); |
+ var fakeDisplayLayout = this.createDisplayLayout( |
+ fakeId, 'Fake Display ' + i, primary.bounds, layoutType, |
+ primary.id); |
+ fakeDisplayLayout.bounds = this.calcLayoutBounds_(fakeDisplayLayout); |
+ gFakeDisplays.push(fakeDisplayLayout); |
+ } |
+ } |
+ }, |
+ |
+ calcLayoutBounds_: function(layout) { |
+ if (layout.parentId == '') |
+ return layout.bounds; |
+ var parent = this.displayLayoutMap_[layout.parentId]; |
+ // Parent layout bounds may not be calculated yet, so calculate (but |
+ // do not set) them. |
+ var parentBounds = this.calcLayoutBounds_(parent); |
+ var left = 0, top = 0; |
+ switch (layout.layoutType) { |
+ case options.DisplayLayoutType.TOP: |
+ left = parentBounds.left + layout.offset; |
+ top = parentBounds.top - layout.bounds.height; |
+ break; |
+ case options.DisplayLayoutType.RIGHT: |
+ left = parentBounds.left + parentBounds.width; |
+ top = parentBounds.top + layout.offset; |
+ break; |
+ case options.DisplayLayoutType.BOTTOM: |
+ left = parentBounds.left + layout.offset; |
+ top = parentBounds.top + parentBounds.height; |
+ break; |
+ case options.DisplayLayoutType.LEFT: |
+ left = parentBounds.left - layout.bounds.width; |
+ top = parentBounds.top + layout.offset; |
+ break; |
+ } |
+ return { |
+ left: left, |
+ top: top, |
+ width: layout.bounds.width, |
+ height: layout.bounds.height |
+ }; |
+ } |
+ |
+ }; |
+ |
+ // Export |
+ return {DisplayLayoutManagerMulti: DisplayLayoutManagerMulti}; |
+}); |