| OLD | NEW |
| 1 // Copyright 2016 The Chromium Authors. All rights reserved. | 1 // Copyright 2016 The Chromium Authors. All rights reserved. |
| 2 // Use of this source code is governed by a BSD-style license that can be | 2 // Use of this source code is governed by a BSD-style license that can be |
| 3 // found in the LICENSE file. | 3 // found in the LICENSE file. |
| 4 | 4 |
| 5 /** | 5 /** |
| 6 * @fileoverview | 6 * @fileoverview |
| 7 * 'display-layout' presents a visual representation of the layout of one or | 7 * 'display-layout' presents a visual representation of the layout of one or |
| 8 * more displays and allows them to be arranged. | 8 * more displays and allows them to be arranged. |
| 9 */ | 9 */ |
| 10 | 10 |
| 11 (function() { | 11 (function() { |
| 12 | 12 |
| 13 /** @const {number} */ var MIN_VISUAL_SCALE = .01; | 13 /** @const {number} */ var MIN_VISUAL_SCALE = .01; |
| 14 | 14 |
| 15 Polymer({ | 15 Polymer({ |
| 16 is: 'display-layout', | 16 is: 'display-layout', |
| 17 | 17 |
| 18 behaviors: [ | 18 behaviors: [ |
| 19 Polymer.IronResizableBehavior, | 19 Polymer.IronResizableBehavior, |
| 20 DragBehavior, | 20 DragBehavior, |
| 21 LayoutBehavior, |
| 21 ], | 22 ], |
| 22 | 23 |
| 23 properties: { | 24 properties: { |
| 24 /** | 25 /** |
| 25 * Array of displays. | 26 * Array of displays. |
| 26 * @type {!Array<!chrome.system.display.DisplayUnitInfo>} | 27 * @type {!Array<!chrome.system.display.DisplayUnitInfo>} |
| 27 */ | 28 */ |
| 28 displays: Array, | 29 displays: Array, |
| 29 | 30 |
| 30 /** | 31 /** |
| 31 * Array of display layouts. | |
| 32 * @type {!Array<!chrome.system.display.DisplayLayout>} | |
| 33 */ | |
| 34 layouts: Array, | |
| 35 | |
| 36 /** | |
| 37 * Whether or not mirroring is enabled. | 32 * Whether or not mirroring is enabled. |
| 38 * @type {boolean} | 33 * @type {boolean} |
| 39 */ | 34 */ |
| 40 mirroring: false, | 35 mirroring: false, |
| 41 | 36 |
| 42 /** @type {!chrome.system.display.DisplayUnitInfo|undefined} */ | 37 /** @type {!chrome.system.display.DisplayUnitInfo|undefined} */ |
| 43 selectedDisplay: Object, | 38 selectedDisplay: Object, |
| 44 | 39 |
| 45 /** | 40 /** |
| 46 * The ratio of the display area div (in px) to DisplayUnitInfo.bounds. | 41 * The ratio of the display area div (in px) to DisplayUnitInfo.bounds. |
| 47 * @type {number} | 42 * @type {number} |
| 48 */ | 43 */ |
| 49 visualScale: 1, | 44 visualScale: 1, |
| 50 }, | 45 }, |
| 51 | 46 |
| 52 /** @private {!Object<chrome.system.display.Bounds>} */ | |
| 53 displayBoundsMap_: {}, | |
| 54 | |
| 55 /** @private {!Object<chrome.system.display.DisplayLayout>} */ | |
| 56 layoutMap_: {}, | |
| 57 | |
| 58 /** | |
| 59 * The calculated bounds used for generating the div bounds. | |
| 60 * @private {!Object<chrome.system.display.Bounds>} | |
| 61 */ | |
| 62 calculatedBoundsMap_: {}, | |
| 63 | |
| 64 /** @private {!{left: number, top: number}} */ | 47 /** @private {!{left: number, top: number}} */ |
| 65 visualOffset_: {left: 0, top: 0}, | 48 visualOffset_: {left: 0, top: 0}, |
| 66 | 49 |
| 67 /** @override */ | 50 /** @override */ |
| 68 attached: function() { | 51 attached: function() { |
| 69 // TODO(stevenjb): Remove retry once fixed: | 52 // TODO(stevenjb): Remove retry once fixed: |
| 70 // https://github.com/Polymer/polymer/issues/3629 | 53 // https://github.com/Polymer/polymer/issues/3629 |
| 71 var self = this; | 54 var self = this; |
| 72 var retry = 100; // ms | 55 var retry = 100; // ms |
| 73 function tryCalcVisualScale() { | 56 function tryCalcVisualScale() { |
| 74 if (!self.calculateVisualScale_()) | 57 if (!self.calculateVisualScale_()) |
| 75 setTimeout(tryCalcVisualScale, retry); | 58 setTimeout(tryCalcVisualScale, retry); |
| 76 } | 59 } |
| 77 tryCalcVisualScale(); | 60 tryCalcVisualScale(); |
| 78 }, | 61 }, |
| 79 | 62 |
| 80 /** @override */ | 63 /** @override */ |
| 81 detached: function() { | 64 detached: function() { this.initializeDrag(false); }, |
| 82 this.initializeDrag(false); | |
| 83 }, | |
| 84 | 65 |
| 85 /** | 66 /** |
| 86 * Called explicitly when |this.displays| and their associated |this.layouts| | 67 * Called explicitly when |this.displays| and their associated |this.layouts| |
| 87 * have been fetched from chrome. | 68 * have been fetched from chrome. |
| 88 * @param {!Array<!chrome.system.display.DisplayUnitInfo>} displays | 69 * @param {!Array<!chrome.system.display.DisplayUnitInfo>} displays |
| 89 * @param {!Array<!chrome.system.display.DisplayLayout>} layouts | 70 * @param {!Array<!chrome.system.display.DisplayLayout>} layouts |
| 90 */ | 71 */ |
| 91 updateDisplays: function(displays, layouts) { | 72 updateDisplays: function(displays, layouts) { |
| 92 this.displays = displays; | 73 this.displays = displays; |
| 93 this.layouts = layouts; | 74 this.layouts = layouts; |
| 94 | 75 |
| 95 this.mirroring = displays.length > 0 && !!displays[0].mirroringSourceId; | 76 this.mirroring = displays.length > 0 && !!displays[0].mirroringSourceId; |
| 96 | 77 |
| 97 this.displayBoundsMap_ = {}; | 78 this.initializeDisplayLayout(displays, layouts); |
| 98 for (let display of this.displays) | |
| 99 this.displayBoundsMap_[display.id] = display.bounds; | |
| 100 | |
| 101 this.layoutMap_ = {}; | |
| 102 for (let layout of this.layouts) | |
| 103 this.layoutMap_[layout.id] = layout; | |
| 104 | |
| 105 this.calculatedBoundsMap_ = {}; | |
| 106 for (let display of this.displays) | |
| 107 this.calculateBounds_(display.id, display.bounds); | |
| 108 | 79 |
| 109 this.calculateVisualScale_(); | 80 this.calculateVisualScale_(); |
| 110 | 81 |
| 111 this.initializeDrag( | 82 this.initializeDrag( |
| 112 !this.mirroring, this.$.displayArea, this.onDrag_.bind(this)); | 83 !this.mirroring, this.$.displayArea, this.onDrag_.bind(this)); |
| 113 }, | 84 }, |
| 114 | 85 |
| 115 /** | 86 /** |
| 116 * Calculates the visual offset and scale for the display area | 87 * Calculates the visual offset and scale for the display area |
| 117 * (i.e. the ratio of the display area div size to the area required to | 88 * (i.e. the ratio of the display area div size to the area required to |
| 118 * contain the DisplayUnitInfo bounding boxes). | 89 * contain the DisplayUnitInfo bounding boxes). |
| 119 * @return {boolean} Whether the calculation was successful. | 90 * @return {boolean} Whether the calculation was successful. |
| 120 * @private | 91 * @private |
| 121 */ | 92 */ |
| 122 calculateVisualScale_() { | 93 calculateVisualScale_() { |
| 123 var displayAreaDiv = this.$.displayArea; | 94 var displayAreaDiv = this.$.displayArea; |
| 124 if (!displayAreaDiv || !displayAreaDiv.offsetWidth || !this.displays || | 95 if (!displayAreaDiv || !displayAreaDiv.offsetWidth || !this.displays || |
| 125 !this.displays.length) { | 96 !this.displays.length) { |
| 126 return false; | 97 return false; |
| 127 } | 98 } |
| 128 | 99 |
| 129 var display = this.displays[0]; | 100 var display = this.displays[0]; |
| 130 var bounds = this.calculatedBoundsMap_[display.id]; | 101 var bounds = this.getCalculatedDisplayBounds(display.id); |
| 131 var displayInfoBoundingBox = { | 102 var boundsBoundingBox = { |
| 132 left: bounds.left, | 103 left: bounds.left, |
| 133 right: bounds.left + bounds.width, | 104 right: bounds.left + bounds.width, |
| 134 top: bounds.top, | 105 top: bounds.top, |
| 135 bottom: bounds.top + bounds.height, | 106 bottom: bounds.top + bounds.height, |
| 136 }; | 107 }; |
| 137 var maxWidth = bounds.width; | 108 var maxWidth = bounds.width; |
| 138 var maxHeight = bounds.height; | 109 var maxHeight = bounds.height; |
| 139 for (let i = 1; i < this.displays.length; ++i) { | 110 for (let i = 1; i < this.displays.length; ++i) { |
| 140 display = this.displays[i]; | 111 display = this.displays[i]; |
| 141 bounds = this.calculatedBoundsMap_[display.id]; | 112 bounds = this.getCalculatedDisplayBounds(display.id); |
| 142 displayInfoBoundingBox.left = | 113 boundsBoundingBox.left = Math.min(boundsBoundingBox.left, bounds.left); |
| 143 Math.min(displayInfoBoundingBox.left, bounds.left); | 114 boundsBoundingBox.right = |
| 144 displayInfoBoundingBox.right = | 115 Math.max(boundsBoundingBox.right, bounds.left + bounds.width); |
| 145 Math.max(displayInfoBoundingBox.right, bounds.left + bounds.width); | 116 boundsBoundingBox.top = Math.min(boundsBoundingBox.top, bounds.top); |
| 146 displayInfoBoundingBox.top = | 117 boundsBoundingBox.bottom = |
| 147 Math.min(displayInfoBoundingBox.top, bounds.top); | 118 Math.max(boundsBoundingBox.bottom, bounds.top + bounds.height); |
| 148 displayInfoBoundingBox.bottom = | |
| 149 Math.max(displayInfoBoundingBox.bottom, bounds.top + bounds.height); | |
| 150 maxWidth = Math.max(maxWidth, bounds.width); | 119 maxWidth = Math.max(maxWidth, bounds.width); |
| 151 maxHeight = Math.max(maxHeight, bounds.height); | 120 maxHeight = Math.max(maxHeight, bounds.height); |
| 152 } | 121 } |
| 153 | 122 |
| 154 // Create a margin around the bounding box equal to the size of the | 123 // Create a margin around the bounding box equal to the size of the |
| 155 // largest displays. | 124 // largest displays. |
| 156 var displayInfoBoundsWidth = displayInfoBoundingBox.right - | 125 var boundsWidth = boundsBoundingBox.right - boundsBoundingBox.left; |
| 157 displayInfoBoundingBox.left + maxWidth * 2; | 126 var boundsHeight = boundsBoundingBox.bottom - boundsBoundingBox.top; |
| 158 var displayInfoBoundsHeight = displayInfoBoundingBox.bottom - | |
| 159 displayInfoBoundingBox.top + maxHeight * 2; | |
| 160 | 127 |
| 161 // Calculate the scale. | 128 // Calculate the scale. |
| 162 var horizontalScale = displayAreaDiv.offsetWidth / displayInfoBoundsWidth; | 129 var horizontalScale = |
| 163 var verticalScale = displayAreaDiv.offsetHeight / displayInfoBoundsHeight; | 130 displayAreaDiv.offsetWidth / (boundsWidth + maxWidth * 2); |
| 131 var verticalScale = |
| 132 displayAreaDiv.offsetHeight / (boundsHeight + maxHeight * 2); |
| 164 var scale = Math.min(horizontalScale, verticalScale); | 133 var scale = Math.min(horizontalScale, verticalScale); |
| 165 | 134 |
| 166 // Calculate the offset. | 135 // Calculate the offset. |
| 167 this.visualOffset_.left = (-displayInfoBoundingBox.left + maxWidth) * scale; | 136 this.visualOffset_.left = |
| 168 this.visualOffset_.top = (-displayInfoBoundingBox.top + maxHeight) * scale; | 137 ((displayAreaDiv.offsetWidth - (boundsWidth * scale)) / 2) - |
| 138 boundsBoundingBox.left * scale; |
| 139 this.visualOffset_.top = |
| 140 ((displayAreaDiv.offsetHeight - (boundsHeight * scale)) / 2) - |
| 141 boundsBoundingBox.top * scale; |
| 169 | 142 |
| 170 // Update the scale which will trigger calls to getDivStyle_. | 143 // Update the scale which will trigger calls to getDivStyle_. |
| 171 this.visualScale = Math.max(MIN_VISUAL_SCALE, scale); | 144 this.visualScale = Math.max(MIN_VISUAL_SCALE, scale); |
| 172 | 145 |
| 173 return true; | 146 return true; |
| 174 }, | 147 }, |
| 175 | 148 |
| 176 /** | 149 /** |
| 177 * @param {string} id | 150 * @param {string} id |
| 178 * @param {!chrome.system.display.Bounds} displayBounds | 151 * @param {!chrome.system.display.Bounds} displayBounds |
| 179 * @param {number} visualScale | 152 * @param {number} visualScale |
| 180 * @return {string} The style string for the div. | 153 * @return {string} The style string for the div. |
| 181 * @private | 154 * @private |
| 182 */ | 155 */ |
| 183 getDivStyle_: function(id, displayBounds, visualScale) { | 156 getDivStyle_: function(id, displayBounds, visualScale) { |
| 184 // This matches the size of the box-shadow or border in CSS. | 157 // This matches the size of the box-shadow or border in CSS. |
| 185 /** @const {number} */ var BORDER = 2; | 158 /** @const {number} */ var BORDER = 2; |
| 186 var bounds = this.calculatedBoundsMap_[id]; | 159 var bounds = this.getCalculatedDisplayBounds(id); |
| 187 var height = Math.round(bounds.height * this.visualScale) - BORDER * 2; | 160 var height = Math.round(bounds.height * this.visualScale) - BORDER * 2; |
| 188 var width = Math.round(bounds.width * this.visualScale) - BORDER * 2; | 161 var width = Math.round(bounds.width * this.visualScale) - BORDER * 2; |
| 189 var left = | 162 var left = |
| 190 Math.round(this.visualOffset_.left + (bounds.left * this.visualScale)); | 163 Math.round(this.visualOffset_.left + (bounds.left * this.visualScale)); |
| 191 var top = | 164 var top = |
| 192 Math.round(this.visualOffset_.top + (bounds.top * this.visualScale)); | 165 Math.round(this.visualOffset_.top + (bounds.top * this.visualScale)); |
| 193 return `height: ${height}px; width: ${width}px;` + | 166 return `height: ${height}px; width: ${width}px;` + |
| 194 ` left: ${left}px; top: ${top}px`; | 167 ` left: ${left}px; top: ${top}px`; |
| 195 }, | 168 }, |
| 196 | 169 |
| 197 /** | 170 /** |
| 198 * @param {!chrome.system.display.DisplayUnitInfo} display | 171 * @param {!chrome.system.display.DisplayUnitInfo} display |
| 199 * @param {!chrome.system.display.DisplayUnitInfo} selectedDisplay | 172 * @param {!chrome.system.display.DisplayUnitInfo} selectedDisplay |
| 200 * @return {boolean} | 173 * @return {boolean} |
| 201 * @private | 174 * @private |
| 202 */ | 175 */ |
| 203 isSelected_: function(display, selectedDisplay) { | 176 isSelected_: function(display, selectedDisplay) { |
| 204 return display.id == selectedDisplay.id; | 177 return display.id == selectedDisplay.id; |
| 205 }, | 178 }, |
| 206 | 179 |
| 207 /** | 180 /** |
| 208 * @param {!{model: !{index: number}}} e | 181 * @param {!{model: !{item: !chrome.system.display.DisplayUnitInfo}, |
| 182 * target: !PaperButtonElement}} e |
| 209 * @private | 183 * @private |
| 210 */ | 184 */ |
| 211 onSelectDisplayTap_: function(e) { | 185 onSelectDisplayTap_: function(e) { |
| 212 this.fire('select-display', e.model.index); | 186 this.fire('select-display', e.model.item.id); |
| 187 // Force active in case the selected display was clicked. |
| 188 e.target.active = true; |
| 213 }, | 189 }, |
| 214 | 190 |
| 215 /** | 191 /** |
| 216 * Recursively calculate the bounds of a display relative to its parents. | |
| 217 * Caches the display bounds so that parent bounds are only calculated once. | |
| 218 * TODO(stevenjb): Move this function and the maps it requires to a separate | |
| 219 * behavior which will include snapping and collisions. | |
| 220 * @param {string} id | |
| 221 * @param {!chrome.system.display.Bounds} bounds | |
| 222 * @private | |
| 223 */ | |
| 224 calculateBounds_: function(id, bounds) { | |
| 225 if (id in this.calculatedBoundsMap_) | |
| 226 return; // Already calculated (i.e. a parent of a previous display) | |
| 227 var left, top; | |
| 228 var layout = this.layoutMap_[id]; | |
| 229 if (!layout || !layout.parentId) { | |
| 230 left = -bounds.width / 2; | |
| 231 top = -bounds.height / 2; | |
| 232 } else { | |
| 233 var parentDisplayBounds = this.displayBoundsMap_[layout.parentId]; | |
| 234 var parentBounds; | |
| 235 if (!(layout.parentId in this.calculatedBoundsMap_)) | |
| 236 this.calculateBounds_(layout.parentId, parentDisplayBounds); | |
| 237 parentBounds = this.calculatedBoundsMap_[layout.parentId]; | |
| 238 left = parentBounds.left; | |
| 239 top = parentBounds.top; | |
| 240 switch (layout.position) { | |
| 241 case chrome.system.display.LayoutPosition.TOP: | |
| 242 top -= bounds.height; | |
| 243 break; | |
| 244 case chrome.system.display.LayoutPosition.RIGHT: | |
| 245 left += parentBounds.width; | |
| 246 break; | |
| 247 case chrome.system.display.LayoutPosition.BOTTOM: | |
| 248 top += parentBounds.height; | |
| 249 break; | |
| 250 case chrome.system.display.LayoutPosition.LEFT: | |
| 251 left -= bounds.height; | |
| 252 break; | |
| 253 } | |
| 254 } | |
| 255 var result = { | |
| 256 left: left, | |
| 257 top: top, | |
| 258 width: bounds.width, | |
| 259 height: bounds.height | |
| 260 }; | |
| 261 this.calculatedBoundsMap_[id] = result; | |
| 262 }, | |
| 263 | |
| 264 /** | |
| 265 * @param {string} id | 192 * @param {string} id |
| 266 * @param {?DragPosition} amount | 193 * @param {?DragPosition} amount |
| 267 */ | 194 */ |
| 268 onDrag_(id, amount) { | 195 onDrag_(id, amount) { |
| 269 id = id.substr(1); // Skip prefix | 196 id = id.substr(1); // Skip prefix |
| 270 | 197 |
| 271 var newBounds; | 198 var newBounds; |
| 272 if (!amount) { | 199 if (!amount) { |
| 273 // TODO(stevenjb): Resolve layout and send update. | 200 this.finishUpdateDisplayBounds(id); |
| 274 newBounds = this.calculatedBoundsMap_[id]; | 201 newBounds = this.getCalculatedDisplayBounds(id); |
| 275 } else { | 202 } else { |
| 276 // Make sure the dragged display is also selected. | 203 // Make sure the dragged display is also selected. |
| 277 if (id != this.selectedDisplay.id) | 204 if (id != this.selectedDisplay.id) |
| 278 this.fire('select-display', id); | 205 this.fire('select-display', id); |
| 279 | 206 |
| 280 var calculatedBounds = this.calculatedBoundsMap_[id]; | 207 var calculatedBounds = this.getCalculatedDisplayBounds(id); |
| 281 newBounds = | 208 newBounds = |
| 282 /** @type {chrome.system.display.Bounds} */ ( | 209 /** @type {chrome.system.display.Bounds} */ ( |
| 283 Object.assign({}, calculatedBounds)); | 210 Object.assign({}, calculatedBounds)); |
| 284 newBounds.left += Math.round(amount.x / this.visualScale); | 211 newBounds.left += Math.round(amount.x / this.visualScale); |
| 285 newBounds.top += Math.round(amount.y / this.visualScale); | 212 newBounds.top += Math.round(amount.y / this.visualScale); |
| 286 // TODO(stevenjb): Update layout. | 213 newBounds = this.updateDisplayBounds(id, newBounds); |
| 287 } | 214 } |
| 288 var left = | 215 var left = |
| 289 this.visualOffset_.left + Math.round(newBounds.left * this.visualScale); | 216 this.visualOffset_.left + Math.round(newBounds.left * this.visualScale); |
| 290 var top = | 217 var top = |
| 291 this.visualOffset_.top + Math.round(newBounds.top * this.visualScale); | 218 this.visualOffset_.top + Math.round(newBounds.top * this.visualScale); |
| 292 var div = this.$$('#_' + id); | 219 var div = this.$$('#_' + id); |
| 293 div.style.left = '' + left + 'px'; | 220 div.style.left = '' + left + 'px'; |
| 294 div.style.top = '' + top + 'px'; | 221 div.style.top = '' + top + 'px'; |
| 295 }, | 222 }, |
| 296 | 223 |
| 297 }); | 224 }); |
| 298 | 225 |
| 299 })(); | 226 })(); |
| OLD | NEW |