blob: e26becaa38fcaaba2c189b777fa12d3ea726bf96 [file] [log] [blame]
// 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;
/**
* @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 */
setFocusedId: function(focusedId, opt_userAction) {
DisplayLayoutManager.prototype.setFocusedId.call(this, focusedId);
if (!opt_userAction || this.dragId_ != '')
return;
var layout = this.displayLayoutMap_[focusedId];
this.highlightEdge_(layout.parentId, layout.layoutType);
},
/** @override */
updatePosition: function(id, newPosition) {
this.dragId_ = id;
var layout = this.displayLayoutMap_[id];
// Find the closest parent.
var parentId = this.findClosest_(layout, newPosition);
var parent = this.displayLayoutMap_[parentId];
// Find the closest edge.
var layoutType = layout.getLayoutTypeForPosition(parent.div, newPosition);
// Calculate the new position and delta.
var oldPos = {x: layout.div.offsetLeft, y: layout.div.offsetTop};
var newPos = layout.getSnapPosition(newPosition, parent.div, layoutType);
var deltaPos = {x: newPos.x - oldPos.x, y: newPos.y - oldPos.y};
// Check for collisions.
this.collideAndModifyDelta_(layout, deltaPos);
if (deltaPos.x == 0 && deltaPos.y == 0)
return;
// Update the div (but not the actual layout type or offset yet),
newPos = {x: oldPos.x + deltaPos.x, y: oldPos.y + deltaPos.y};
layout.setDivPosition(newPos, parent.div, layoutType);
// If the edge changed, update and highlight it.
if (layoutType != this.dragLayoutType_ ||
parentId != this.dragParentId_) {
this.dragLayoutType_ = layoutType;
this.dragParentId_ = parentId;
this.highlightEdge_(this.dragParentId_, this.dragLayoutType_);
}
},
/** @override */
finalizePosition: function(id) {
if (id != this.dragId_ || !this.dragParentId_) {
this.dragId_ = '';
return false;
}
var layout = this.displayLayoutMap_[id];
// All immediate children of |layout| will need to be re-parented.
var orphanIds = this.findChildren_(id, false /* do not recurse */);
if (layout.parentId == '') {
// We can not re-parent the primary display, so instead re-parent the
// "drag parent" to the primary display and make all other displays
// orphans.
orphanIds = this.findChildren_(id, true /* recurse */);
// Remove the "drag parent" from the orphans list and reparent it first.
var index = orphanIds.indexOf(this.dragParentId_);
assert(index != -1);
orphanIds.splice(index, 1);
this.reparentOrphan_(this.dragParentId_, orphanIds);
} else {
var dragParent = this.displayLayoutMap_[this.dragParentId_];
// Special case: re-parenting to a descendant. Parent the immediate
// child (the 'top parent') to the dragged display's current parent.
var topParent = dragParent;
while (topParent) {
if (topParent.parentId == '')
break;
if (topParent.parentId == id) {
topParent.parentId = layout.parentId;
break;
}
topParent = this.displayLayoutMap_[topParent.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.
layout.adjustCorners(dragParent.div, this.dragLayoutType_);
layout.calculateOffset(this.visualScale_, dragParent);
}
// Update any orphaned children. This may cause the dragged display to
// be re-attached if it was attached to a child.
this.updateOrphans_(orphanIds);
this.highlightEdge_('', undefined); // Remove any highlights.
this.dragId_ = '';
return layout.originalDivOffsets.x != layout.div.offsetLeft ||
layout.originalDivOffsets.y != layout.div.offsetTop;
},
/**
* Re-parent all entries in |orphanIds| and any children.
* @param {Array<string>} orphanIds The list of ids affected by the move.
* @private
*/
updateOrphans_: function(orphanIds) {
var orphans = orphanIds.slice();
for (var orphan of orphanIds) {
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.indexOf(o) == -1)
orphans.push(o);
}
}
// Remove each orphan from the list as it is re-parented so that
// subsequent orphans can be parented to it.
while (orphans.length) {
var orphanId = orphans.shift();
this.reparentOrphan_(orphanId, orphans);
}
},
/**
* Re-parent the orphan to a layout that is not a member of
* |otherOrphanIds|.
* @param {string} orphanId The id of the orphan to re-parent.
* @param {Array<string>} otherOrphanIds The list of ids of other orphans
* to ignore when re-parenting.
* @private
*/
reparentOrphan_: function(orphanId, otherOrphanIds) {
var orphan = this.displayLayoutMap_[orphanId];
if (orphanId == this.dragId_ && orphan.parentId != '') {
// Preserve the layout and offset of the dragged div.
var parent = this.displayLayoutMap_[orphan.parentId];
orphan.bounds = this.calcLayoutBounds_(orphan);
orphan.layoutDivFromBounds(
this.displayAreaOffset_, this.visualScale_, parent);
return;
}
// Find the closest parent.
var pos = {x: orphan.div.offsetLeft, y: orphan.div.offsetTop};
var newParentId = this.findClosest_(orphan, pos, otherOrphanIds);
orphan.parentId = newParentId;
assert(newParentId != '');
var parent = this.displayLayoutMap_[newParentId];
// Find the closest edge.
var layoutType = orphan.getLayoutTypeForPosition(parent.div, pos);
// Snap from the nearest corner to the desired locaiton and get the delta.
var cornerPos = orphan.getCornerPos(parent.div, pos);
pos = orphan.getSnapPosition(pos, parent.div, layoutType);
var deltaPos = {x: pos.x - cornerPos.x, y: pos.y - cornerPos.y};
// Check for collisions.
this.collideAndModifyDelta_(orphan, deltaPos);
pos = {x: cornerPos.x + deltaPos.x, y: cornerPos.y + deltaPos.y};
// Update the div and adjust the corners.
orphan.layoutType = layoutType;
orphan.setDivPosition(pos, parent.div, layoutType);
orphan.adjustCorners(parent.div, layoutType);
// Calculate the bounds from the new div position.
orphan.calculateOffset(this.visualScale_, parent);
// TODO(stevenjb): Set bounds and send orphan update.
},
/**
* @param {string} parentId
* @param {boolean} recurse Include descendants of children.
* @return {!Array<string>}
* @private
*/
findChildren_: function(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_ignoreIds Ids to ignore.
* @return {string}
* @private
*/
findClosest_: function(child, position, opt_ignoreIds) {
var x = position.x + child.div.offsetWidth / 2;
var y = position.y + child.div.offsetHeight / 2;
var closestId = '';
var closestDelta2 = 0;
for (var id in this.displayLayoutMap_) {
if (id == child.id)
continue;
if (opt_ignoreIds && opt_ignoreIds.indexOf(id) != -1)
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;
},
/**
* Intersects |layout| with each other layout and reduces |deltaPos| to
* avoid any collisions (or sets it to [0,0] if the div can not be moved
* in the direction of |deltaPos|).
* @param {!options.DisplayLayout} layout
* @param {!options.DisplayPosition} deltaPos
*/
collideAndModifyDelta_: function(layout, deltaPos) {
var keys = Object.keys(
/** @type {!Object<!options.DisplayLayout>}*/ (
this.displayLayoutMap_));
var others = new Set(keys);
others.delete(layout.id);
var checkCollisions = true;
while (checkCollisions) {
checkCollisions = false;
for (var tid of others) {
var tlayout = this.displayLayoutMap_[tid];
if (layout.collideWithDivAndModifyDelta(tlayout.div, deltaPos)) {
if (deltaPos.x == 0 && deltaPos.y == 0)
return;
others.delete(tid);
checkCollisions = true;
break;
}
}
}
},
/**
* Highlights the edge of the div associated with |parentId| based on
* |layoutType| and removes any other highlights. If |layoutType| is
* undefined, removes all highlights.
* @param {string} id
* @param {options.DisplayLayoutType|undefined} layoutType
* @private
*/
highlightEdge_: function(id, layoutType) {
for (var tid in this.displayLayoutMap_) {
var tlayout = this.displayLayoutMap_[tid];
var highlight = '';
if (tlayout.id == id) {
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');
}
},
/**
* Calculates but do not set the div bound.
* @param {!options.DisplayLayout} layout
* @return {!options.DisplayBounds}
*/
calcLayoutBounds_: function(layout) {
if (layout.parentId == '')
return /** @type {!options.DisplayBounds} */ (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);
return layout.calculateBounds(parentBounds);
},
};
// Export
return {DisplayLayoutManagerMulti: DisplayLayoutManagerMulti};
});