OLD | NEW |
(Empty) | |
| 1 /** |
| 2 * @fileoverview Description of this file. |
| 3 */ |
| 4 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 5 // Use of this source code is governed by a BSD-style license that can be |
| 6 // found in the LICENSE file. |
| 7 |
| 8 cr.exportPath('options'); |
| 9 |
| 10 cr.define('options', function() { |
| 11 'use strict'; |
| 12 |
| 13 var DisplayLayoutManager = options.DisplayLayoutManager; |
| 14 |
| 15 var sIdPrefix = 'fakeId'; |
| 16 |
| 17 var gFakeDisplayNum = 3; |
| 18 var gFakeDisplays = []; |
| 19 |
| 20 /** |
| 21 * @constructor |
| 22 * @extends {options.DisplayLayoutManager} |
| 23 */ |
| 24 function DisplayLayoutManagerMulti() { |
| 25 DisplayLayoutManager.call(this); |
| 26 } |
| 27 |
| 28 // Helper class for display layout management. Implements logic for laying |
| 29 // out 3+ displays. |
| 30 DisplayLayoutManagerMulti.prototype = { |
| 31 __proto__: DisplayLayoutManager.prototype, |
| 32 |
| 33 /** @type {string} */ |
| 34 dragId_: '', |
| 35 |
| 36 /** @type {string} */ |
| 37 dragParentId_: '', |
| 38 |
| 39 /** @type {options.DisplayLayoutType} */ |
| 40 dragLayoutType_: options.DisplayLayoutType.RIGHT, |
| 41 |
| 42 /** @override */ |
| 43 createDisplayArea: function(displayAreaDiv, minVisualScale) { |
| 44 // Add fake displays just before creating divs. |
| 45 this.createFakeDisplays_(gFakeDisplayNum); |
| 46 for (var i = 0; i < gFakeDisplayNum; ++i) { |
| 47 var fake = gFakeDisplays[i]; |
| 48 this.displayLayoutMap_[fake.id] = fake; |
| 49 } |
| 50 return DisplayLayoutManager.prototype.createDisplayArea.call( |
| 51 this, displayAreaDiv, minVisualScale); |
| 52 }, |
| 53 |
| 54 /** @override */ |
| 55 setFocusedId: function(focusedId) { |
| 56 DisplayLayoutManager.prototype.setFocusedId.call(this, focusedId); |
| 57 var layout = this.displayLayoutMap_[focusedId]; |
| 58 this.highlightParentEdge_(layout.parentId, layout.layoutType); |
| 59 }, |
| 60 |
| 61 /** @override */ |
| 62 updatePosition: function(id, newPosition) { |
| 63 this.dragId_ = id; |
| 64 var layout = this.displayLayoutMap_[id]; |
| 65 this.dragParentId_ = this.findClosest_(layout, newPosition); |
| 66 var parent = this.displayLayoutMap_[this.dragParentId_]; |
| 67 this.dragLayoutType_ = |
| 68 this.getLayoutTypeForPosition_(layout.div, parent.div, newPosition); |
| 69 this.updateDivPosition_( |
| 70 layout.div, newPosition, parent.div, this.dragLayoutType_); |
| 71 this.highlightParentEdge_(this.dragParentId_, this.dragLayoutType_); |
| 72 }, |
| 73 |
| 74 /** @override */ |
| 75 finalizePosition: function(id) { |
| 76 if (id != this.dragId_) |
| 77 return false; |
| 78 |
| 79 var layout = this.displayLayoutMap_[id]; |
| 80 var parent = this.displayLayoutMap_[this.dragParentId_]; |
| 81 |
| 82 // All immediate children of |layout| need to be re-parented. |
| 83 var orphans = this.findChildren_(id, false /* do not recurse */); |
| 84 |
| 85 // Special case: re-parenting to a descendant. Parent the immediate child |
| 86 // to the dragged display's parent and treat that as the moved layout. |
| 87 var newParent = parent; |
| 88 while (newParent) { |
| 89 if (newParent.parentId == '') |
| 90 break; |
| 91 if (newParent.parentId == id) { |
| 92 newParent.parentId = layout.parentId; |
| 93 break; |
| 94 } |
| 95 newParent = this.displayLayoutMap_[newParent.parentId]; |
| 96 } |
| 97 |
| 98 // Re-parent the dragged display. |
| 99 layout.parentId = this.dragParentId_; |
| 100 layout.layoutType = this.dragLayoutType_; |
| 101 |
| 102 // Snap to corners and calculate the offset from the new parent. This does |
| 103 // not depend on any unresolved child layout. |
| 104 this.adjustCorners_(layout.div, parent.div, this.dragLayoutType_); |
| 105 this.calculateOffset_(layout); |
| 106 |
| 107 // Update any orphaned children, which may include |id|. |
| 108 this.updateOrphans_(orphans); |
| 109 |
| 110 // Update the fake displays. |
| 111 for (var i = 0; i < gFakeDisplayNum; ++i) { |
| 112 var fakeId = gFakeDisplays[i].id; |
| 113 var fake = Object.assign({}, this.displayLayoutMap_[fakeId]); |
| 114 gFakeDisplays[i] = fake; |
| 115 fake.bounds = this.calcLayoutBounds_(fake); |
| 116 } |
| 117 |
| 118 return layout.originalDivOffsets.x != layout.div.offsetLeft || |
| 119 layout.originalDivOffsets.y != layout.div.offsetTop; |
| 120 }, |
| 121 |
| 122 /** |
| 123 * @param {Array<string>=} orphanedIds The list of ids affected by the move. |
| 124 * @private |
| 125 */ |
| 126 updateOrphans_(orphanedIds) { |
| 127 var orphans = orphanedIds.slice(); |
| 128 for (var orphan of orphanedIds) { |
| 129 var newOrphans = this.findChildren_(orphan, true /* recurse */); |
| 130 // If the dragged display was re-parented to one of its children, |
| 131 // there may be duplicates so merge the lists. |
| 132 for (var o of newOrphans) { |
| 133 if (!orphans.includes(o)) |
| 134 orphans.push(o); |
| 135 } |
| 136 } |
| 137 |
| 138 // Re-parent each orphan to a layout that is not a member of |orphans|. |
| 139 // We remove each orphan from the list so that subsequent orphans can be |
| 140 // parented to initial (primary) orphans. |
| 141 while (orphans.length) { |
| 142 var childId = orphans.shift(); |
| 143 var child = this.displayLayoutMap_[childId]; |
| 144 |
| 145 if (childId == this.dragId_) { |
| 146 // Preserve the layout and offset of the dragged div. |
| 147 this.calcLayoutBounds_(child); |
| 148 this.layoutDivFromBounds_(child); |
| 149 continue; |
| 150 } |
| 151 |
| 152 var pos = {x: child.div.offsetLeft, y: child.div.offsetTop}; |
| 153 var newParentId = this.findClosest_(child, pos, orphans); |
| 154 child.parentId = newParentId; |
| 155 // If all displays are orphans, newParentId will be empty. |
| 156 if (newParentId != '') { |
| 157 var parent = this.displayLayoutMap_[newParentId]; |
| 158 var layoutType = |
| 159 this.getLayoutTypeForPosition_(child.div, parent.div, pos); |
| 160 this.updateDivPosition_(child.div, pos, parent.div, layoutType); |
| 161 this.adjustCorners_(child.div, parent.div, child.layoutType); |
| 162 this.calculateOffset_(child); |
| 163 } |
| 164 // TODO(stevenjb): Set bounds and send child update. |
| 165 } |
| 166 }, |
| 167 |
| 168 /** |
| 169 * @param {string} parentId |
| 170 * @param {boolean} recurse Include descendants of children. |
| 171 * @return {!Array<string>} |
| 172 * @private |
| 173 */ |
| 174 findChildren_(parentId, recurse) { |
| 175 var children = []; |
| 176 for (var childId in this.displayLayoutMap_) { |
| 177 if (childId == parentId) |
| 178 continue; |
| 179 if (this.displayLayoutMap_[childId].parentId == parentId) { |
| 180 // Insert immediate children at the front of the array. |
| 181 children.unshift(childId); |
| 182 if (recurse) { |
| 183 // Descendants get added to the end of the list. |
| 184 children = children.concat(this.findChildren_(childId, true)); |
| 185 } |
| 186 } |
| 187 } |
| 188 return children; |
| 189 }, |
| 190 |
| 191 /** |
| 192 * Finds the display closest to |position| ignoring |child|. |
| 193 * @param {options.DisplayLayout} child |
| 194 * @param {options.DisplayPosition} position |
| 195 * @param {Array<string>=} opt_ignore Ids to ignore. |
| 196 * @return {string} |
| 197 * @private |
| 198 */ |
| 199 findClosest_: function(child, position, opt_ignore) { |
| 200 var closestDist = undefined; |
| 201 var x = position.x + child.div.offsetWidth / 2; |
| 202 var y = position.y + child.div.offsetHeight / 2; |
| 203 var closestId = '', closestDelta2; |
| 204 for (var id in this.displayLayoutMap_) { |
| 205 if (id == child.id) |
| 206 continue; |
| 207 if (opt_ignore && opt_ignore.includes(id)) |
| 208 continue; |
| 209 var div = this.displayLayoutMap_[id].div; |
| 210 var left = div.offsetLeft; |
| 211 var top = div.offsetTop; |
| 212 var width = div.offsetWidth; |
| 213 var height = div.offsetHeight; |
| 214 if (x >= left && x < left + width && y >= top && y < top + height) |
| 215 return id; // point is inside rect |
| 216 var dx, dy; |
| 217 if (x < left) |
| 218 dx = left - x; |
| 219 else if (x > left + width) |
| 220 dx = x - (left + width); |
| 221 else |
| 222 dx = 0; |
| 223 if (y < top) |
| 224 dy = top - y; |
| 225 else if (y > top + height) |
| 226 dy = y - (top + height); |
| 227 else |
| 228 dy = 0; |
| 229 var delta2 = dx * dx + dy * dy; |
| 230 if (closestId == '' || delta2 < closestDelta2) { |
| 231 closestId = id; |
| 232 closestDelta2 = delta2; |
| 233 } |
| 234 } |
| 235 return closestId; |
| 236 }, |
| 237 |
| 238 /** |
| 239 * Calculates the layoutType for |position| relative to |parentDiv|. |
| 240 * @param {?HTMLElement} div |
| 241 * @param {?HTMLElement} parentDiv |
| 242 * @param {options.DisplayPosition} position |
| 243 * @return {options.DisplayLayoutType} |
| 244 * @private |
| 245 */ |
| 246 getLayoutTypeForPosition_: function(div, parentDiv, position) { |
| 247 // Translate position from top-left to center. |
| 248 var x = position.x + div.offsetWidth / 2; |
| 249 var y = position.y + div.offsetHeight / 2; |
| 250 |
| 251 // Determine the distance from the new position to both of the near edges. |
| 252 var div = parentDiv; |
| 253 var left = div.offsetLeft; |
| 254 var top = div.offsetTop; |
| 255 var width = div.offsetWidth; |
| 256 var height = div.offsetHeight; |
| 257 // Signed deltas to center. |
| 258 var dx = x - (left + width / 2); |
| 259 var dy = y - (top + height / 2); |
| 260 // Unsigned distance to each edge. |
| 261 var distx = Math.abs(dx) - width / 2; |
| 262 var disty = Math.abs(dy) - height / 2; |
| 263 if (distx > disty) { |
| 264 if (dx < 0) |
| 265 return options.DisplayLayoutType.LEFT; |
| 266 else |
| 267 return options.DisplayLayoutType.RIGHT; |
| 268 } else { |
| 269 if (dy < 0) |
| 270 return options.DisplayLayoutType.TOP; |
| 271 else |
| 272 return options.DisplayLayoutType.BOTTOM; |
| 273 } |
| 274 }, |
| 275 |
| 276 /** |
| 277 * Update the location |div| to the position closest to |newPosition| along |
| 278 * the edge of |parentDiv| specified by |layoutType|. |
| 279 * @param {?HTMLElement} div |
| 280 * @param {options.DisplayPosition} newPosition |
| 281 * @param {?HTMLElement} parentDiv |
| 282 * @param {!options.DisplayLayoutType} layoutType |
| 283 * @private |
| 284 */ |
| 285 updateDivPosition_(div, newPosition, parentDiv, layoutType) { |
| 286 var snapX = (layoutType == options.DisplayLayoutType.LEFT || |
| 287 layoutType == options.DisplayLayoutType.RIGHT) ? |
| 288 0 /* infinite */ : |
| 289 undefined /* default */; |
| 290 var snapY = (layoutType == options.DisplayLayoutType.TOP || |
| 291 layoutType == options.DisplayLayoutType.BOTTOM) ? |
| 292 0 /* infinite */ : |
| 293 undefined /* default */; |
| 294 newPosition.x = this.snapToEdge_( |
| 295 newPosition.x, div.offsetWidth, parentDiv.offsetLeft, |
| 296 parentDiv.offsetWidth, snapX); |
| 297 newPosition.y = this.snapToEdge_( |
| 298 newPosition.y, div.offsetHeight, parentDiv.offsetTop, |
| 299 parentDiv.offsetHeight, snapY); |
| 300 |
| 301 this.setDivPosition_(div, newPosition, parentDiv, layoutType); |
| 302 }, |
| 303 |
| 304 /** |
| 305 * Highlights the edge of the div associated with |parentId| based on |
| 306 * |layoutType|. |
| 307 * @param {string} parentId |
| 308 * @param {options.DisplayLayoutType} layoutType |
| 309 * @private |
| 310 */ |
| 311 highlightParentEdge_(parentId, layoutType) { |
| 312 for (var tid in this.displayLayoutMap_) { |
| 313 var tlayout = this.displayLayoutMap_[tid]; |
| 314 var highlight = ''; |
| 315 if (tlayout.id == parentId) { |
| 316 switch (layoutType) { |
| 317 case options.DisplayLayoutType.RIGHT: |
| 318 highlight = 'displays-parent-right'; |
| 319 break; |
| 320 case options.DisplayLayoutType.LEFT: |
| 321 highlight = 'displays-parent-left'; |
| 322 break; |
| 323 case options.DisplayLayoutType.TOP: |
| 324 highlight = 'displays-parent-top'; |
| 325 break; |
| 326 case options.DisplayLayoutType.BOTTOM: |
| 327 highlight = 'displays-parent-bottom'; |
| 328 break; |
| 329 } |
| 330 } |
| 331 tlayout.div.classList.toggle( |
| 332 'displays-parent-right', highlight == 'displays-parent-right'); |
| 333 tlayout.div.classList.toggle( |
| 334 'displays-parent-left', highlight == 'displays-parent-left'); |
| 335 tlayout.div.classList.toggle( |
| 336 'displays-parent-top', highlight == 'displays-parent-top'); |
| 337 tlayout.div.classList.toggle( |
| 338 'displays-parent-bottom', highlight == 'displays-parent-bottom'); |
| 339 } |
| 340 }, |
| 341 |
| 342 createFakeDisplays_: function(num) { |
| 343 if (num != gFakeDisplayNum) |
| 344 gFakeDisplays = []; |
| 345 var primary; |
| 346 for (var id in this.displayLayoutMap_) { |
| 347 var layout = this.displayLayoutMap_[id]; |
| 348 if (layout.parentId == '') |
| 349 primary = layout; |
| 350 } |
| 351 for (var i = 0; i < num; ++i) { |
| 352 if (i < gFakeDisplays.length) { |
| 353 gFakeDisplays[i].div = null; |
| 354 } else { |
| 355 var fakeId = sIdPrefix + i; |
| 356 var layoutType = /** @type {options.DisplayLayoutType} */ (i % 4); |
| 357 var fakeDisplayLayout = this.createDisplayLayout( |
| 358 fakeId, 'Fake Display ' + i, primary.bounds, layoutType, |
| 359 primary.id); |
| 360 fakeDisplayLayout.bounds = this.calcLayoutBounds_(fakeDisplayLayout); |
| 361 gFakeDisplays.push(fakeDisplayLayout); |
| 362 } |
| 363 } |
| 364 }, |
| 365 |
| 366 calcLayoutBounds_: function(layout) { |
| 367 if (layout.parentId == '') |
| 368 return layout.bounds; |
| 369 var parent = this.displayLayoutMap_[layout.parentId]; |
| 370 // Parent layout bounds may not be calculated yet, so calculate (but |
| 371 // do not set) them. |
| 372 var parentBounds = this.calcLayoutBounds_(parent); |
| 373 var left = 0, top = 0; |
| 374 switch (layout.layoutType) { |
| 375 case options.DisplayLayoutType.TOP: |
| 376 left = parentBounds.left + layout.offset; |
| 377 top = parentBounds.top - layout.bounds.height; |
| 378 break; |
| 379 case options.DisplayLayoutType.RIGHT: |
| 380 left = parentBounds.left + parentBounds.width; |
| 381 top = parentBounds.top + layout.offset; |
| 382 break; |
| 383 case options.DisplayLayoutType.BOTTOM: |
| 384 left = parentBounds.left + layout.offset; |
| 385 top = parentBounds.top + parentBounds.height; |
| 386 break; |
| 387 case options.DisplayLayoutType.LEFT: |
| 388 left = parentBounds.left - layout.bounds.width; |
| 389 top = parentBounds.top + layout.offset; |
| 390 break; |
| 391 } |
| 392 return { |
| 393 left: left, |
| 394 top: top, |
| 395 width: layout.bounds.width, |
| 396 height: layout.bounds.height |
| 397 }; |
| 398 } |
| 399 |
| 400 }; |
| 401 |
| 402 // Export |
| 403 return {DisplayLayoutManagerMulti: DisplayLayoutManagerMulti}; |
| 404 }); |
OLD | NEW |