1 // Copyright (c) 2012 The Chromium Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style license that can be 3 // found in the LICENSE file. 4 5 cr.define('options', function() { 6 var OptionsPage = options.OptionsPage; 7 8 // The scale ratio of the display rectangle to its original size. 9 /** @const */ var VISUAL_SCALE = 1 / 10; 10 11 // The number of pixels to share the edges between displays. 12 /** @const */ var MIN_OFFSET_OVERLAP = 5; 13 14 /** 15 * Enumeration of secondary display layout. The value has to be same as the 16 * values in ash/display/display_controller.cc. 17 * @enum {number} 18 */ 19 var SecondaryDisplayLayout = { 20 TOP: 0, 21 RIGHT: 1, 22 BOTTOM: 2, 23 LEFT: 3 24 }; 25 26 /** 27 * Calculates the bounds of |element| relative to the page. 28 * @param {HTMLElement} element The element to be known. 29 * @return {Object} The object for the bounds, with x, y, width, and height. 30 */ 31 function getBoundsInPage(element) { 32 var bounds = { 33 x: element.offsetLeft, 34 y: element.offsetTop, 35 width: element.offsetWidth, 36 height: element.offsetHeight 37 }; 38 var parent = element.offsetParent; 39 while (parent && parent != document.body) { 40 bounds.x += parent.offsetLeft; 41 bounds.y += parent.offsetTop; 42 parent = parent.offsetParent; 43 } 44 return bounds; 45 } 46 47 /** 48 * Gets the position of |point| to |rect|, left, right, top, or bottom. 49 * @param {Object} rect The base rectangle with x, y, width, and height. 50 * @param {Object} point The point to check the position. 51 * @return {SecondaryDisplayLayout} The position of the calculated point. 52 */ 53 function getPositionToRectangle(rect, point) { 54 // Separates the area into four (LEFT/RIGHT/TOP/BOTTOM) by the diagonals of 55 // the rect, and decides which area the display should reside. 56 var diagonalSlope = rect.height / rect.width; 57 var topDownIntercept = rect.y - rect.x * diagonalSlope; 58 var bottomUpIntercept = rect.y + rect.height + rect.x * diagonalSlope; 59 60 if (point.y > topDownIntercept + point.x * diagonalSlope) { 61 if (point.y > bottomUpIntercept - point.x * diagonalSlope) 62 return SecondaryDisplayLayout.BOTTOM; 63 else 64 return SecondaryDisplayLayout.LEFT; 65 } else { 66 if (point.y > bottomUpIntercept - point.x * diagonalSlope) 67 return SecondaryDisplayLayout.RIGHT; 68 else 69 return SecondaryDisplayLayout.TOP; 70 } 71 } 72 73 /** 74 * Encapsulated handling of the 'Display' page. 75 * @constructor 76 */ 77 function DisplayOptions() { 78 OptionsPage.call(this, 'display', 79 loadTimeData.getString('displayOptionsPageTabTitle'), 80 'display-options-page'); 81 } 82 83 cr.addSingletonGetter(DisplayOptions); 84 85 DisplayOptions.prototype = { 86 __proto__: OptionsPage.prototype, 87 88 /** 89 * Whether the current output status is mirroring displays or not. 90 * @private 91 */ 92 mirroring_: false, 93 94 /** 95 * The current secondary display layout. 96 * @private 97 */ 98 layout_: SecondaryDisplayLayout.RIGHT, 99 100 /** 101 * The array of current output displays. It also contains the display 102 * rectangles currently rendered on screen. 103 * @private 104 */ 105 displays_: [], 106 107 /** 108 * The index for the currently focused display in the options UI. null if 109 * no one has focus. 110 * @private 111 */ 112 focusedIndex_: null, 113 114 /** 115 * The primary display. 116 * @private 117 */ 118 primaryDisplay_: null, 119 120 /** 121 * The secondary display. 122 * @private 123 */ 124 secondaryDisplay_: null, 125 126 /** 127 * The container div element which contains all of the display rectangles. 128 * @private 129 */ 130 displaysView_: null, 131 132 /** 133 * The scale factor of the actual display size to the drawn display 134 * rectangle size. 135 * @private 136 */ 137 visualScale_: VISUAL_SCALE, 138 139 /** 140 * The location where the last touch event happened. This is used to 141 * prevent unnecessary dragging events happen. Set to null unless it's 142 * during touch events. 143 * @private 144 */ 145 lastTouchLocation_: null, 146 147 /** 148 * Initialize the page. 149 */ 150 initializePage: function() { 151 OptionsPage.prototype.initializePage.call(this); 152 153 $('display-options-toggle-mirroring').onclick = function() { 154 this.mirroring_ = !this.mirroring_; 155 chrome.send('setMirroring', [this.mirroring_]); 156 }.bind(this); 157 158 var container = $('display-options-displays-view-host'); 159 container.onmousemove = this.onMouseMove_.bind(this); 160 window.addEventListener('mouseup', this.endDragging_.bind(this), true); 161 container.ontouchmove = this.onTouchMove_.bind(this); 162 container.ontouchend = this.endDragging_.bind(this); 163 164 $('display-options-set-primary').onclick = function() { 165 chrome.send('setPrimary', [this.displays_[this.focusedIndex_].id]); 166 }.bind(this); 167 $('display-options-resolution-selection').onchange = function(ev) { 168 var display = this.displays_[this.focusedIndex_]; 169 var resolution = display.resolutions[ev.target.value]; 170 if (resolution.scale) { 171 chrome.send('setUIScale', [display.id, resolution.scale]); 172 } else { 173 chrome.send('setResolution', 174 [display.id, resolution.width, resolution.height]); 175 } 176 }.bind(this); 177 $('display-options-orientation-selection').onchange = function(ev) { 178 chrome.send('setOrientation', [this.displays_[this.focusedIndex_].id, 179 ev.target.value]); 180 }.bind(this); 181 $('selected-display-start-calibrating-overscan').onclick = function() { 182 // Passes the target display ID. Do not specify it through URL hash, 183 // we do not care back/forward. 184 var displayOverscan = options.DisplayOverscan.getInstance(); 185 displayOverscan.setDisplayId(this.displays_[this.focusedIndex_].id); 186 OptionsPage.navigateToPage('displayOverscan'); 187 }.bind(this); 188 189 chrome.send('getDisplayInfo'); 190 }, 191 192 /** @override */ 193 didShowPage: function() { 194 var optionTitles = document.getElementsByClassName( 195 'selected-display-option-title'); 196 var maxSize = 0; 197 for (var i = 0; i < optionTitles.length; i++) 198 maxSize = Math.max(maxSize, optionTitles[i].clientWidth); 199 for (var i = 0; i < optionTitles.length; i++) 200 optionTitles[i].style.width = maxSize + 'px'; 201 }, 202 203 /** @override */ 204 onVisibilityChanged_: function() { 205 OptionsPage.prototype.onVisibilityChanged_(this); 206 if (this.visible) 207 chrome.send('getDisplayInfo'); 208 }, 209 210 /** 211 * Mouse move handler for dragging display rectangle. 212 * @param {Event} e The mouse move event. 213 * @private 214 */ 215 onMouseMove_: function(e) { 216 return this.processDragging_(e, {x: e.pageX, y: e.pageY}); 217 }, 218 219 /** 220 * Touch move handler for dragging display rectangle. 221 * @param {Event} e The touch move event. 222 * @private 223 */ 224 onTouchMove_: function(e) { 225 if (e.touches.length != 1) 226 return true; 227 228 var touchLocation = {x: e.touches[0].pageX, y: e.touches[0].pageY}; 229 // Touch move events happen even if the touch location doesn't change, but 230 // it doesn't need to process the dragging. Since sometimes the touch 231 // position changes slightly even though the user doesn't think to move 232 // the finger, very small move is just ignored. 233 /** @const */ var IGNORABLE_TOUCH_MOVE_PX = 1; 234 var xDiff = Math.abs(touchLocation.x - this.lastTouchLocation_.x); 235 var yDiff = Math.abs(touchLocation.y - this.lastTouchLocation_.y); 236 if (xDiff <= IGNORABLE_TOUCH_MOVE_PX && 237 yDiff <= IGNORABLE_TOUCH_MOVE_PX) { 238 return true; 239 } 240 241 this.lastTouchLocation_ = touchLocation; 242 return this.processDragging_(e, touchLocation); 243 }, 244 245 /** 246 * Mouse down handler for dragging display rectangle. 247 * @param {Event} e The mouse down event. 248 * @private 249 */ 250 onMouseDown_: function(e) { 251 if (this.mirroring_) 252 return true; 253 254 if (e.button != 0) 255 return true; 256 257 e.preventDefault(); 258 return this.startDragging_(e.target, {x: e.pageX, y: e.pageY}); 259 }, 260 261 /** 262 * Touch start handler for dragging display rectangle. 263 * @param {Event} e The touch start event. 264 * @private 265 */ 266 onTouchStart_: function(e) { 267 if (this.mirroring_) 268 return true; 269 270 if (e.touches.length != 1) 271 return false; 272 273 e.preventDefault(); 274 var touch = e.touches[0]; 275 this.lastTouchLocation_ = {x: touch.pageX, y: touch.pageY}; 276 return this.startDragging_(e.target, this.lastTouchLocation_); 277 }, 278 279 /** 280 * Collects the current data and sends it to Chrome. 281 * @private 282 */ 283 applyResult_: function() { 284 // Offset is calculated from top or left edge. 285 var primary = this.primaryDisplay_; 286 var secondary = this.secondaryDisplay_; 287 var offset; 288 if (this.layout_ == SecondaryDisplayLayout.LEFT || 289 this.layout_ == SecondaryDisplayLayout.RIGHT) { 290 offset = secondary.div.offsetTop - primary.div.offsetTop; 291 } else { 292 offset = secondary.div.offsetLeft - primary.div.offsetLeft; 293 } 294 chrome.send('setDisplayLayout', 295 [this.layout_, offset / this.visualScale_]); 296 }, 297 298 /** 299 * Snaps the region [point, width] to [basePoint, baseWidth] if 300 * the [point, width] is close enough to the base's edge. 301 * @param {number} point The starting point of the region. 302 * @param {number} width The width of the region. 303 * @param {number} basePoint The starting point of the base region. 304 * @param {number} baseWidth The width of the base region. 305 * @return {number} The moved point. Returns point itself if it doesn't 306 * need to snap to the edge. 307 * @private 308 */ 309 snapToEdge_: function(point, width, basePoint, baseWidth) { 310 // If the edge of the regions is smaller than this, it will snap to the 311 // base's edge. 312 /** @const */ var SNAP_DISTANCE_PX = 16; 313 314 var startDiff = Math.abs(point - basePoint); 315 var endDiff = Math.abs(point + width - (basePoint + baseWidth)); 316 // Prefer the closer one if both edges are close enough. 317 if (startDiff < SNAP_DISTANCE_PX && startDiff < endDiff) 318 return basePoint; 319 else if (endDiff < SNAP_DISTANCE_PX) 320 return basePoint + baseWidth - width; 321 322 return point; 323 }, 324 325 /** 326 * Processes the actual dragging of display rectangle. 327 * @param {Event} e The event which triggers this drag. 328 * @param {Object} eventLocation The location where the event happens. 329 * @private 330 */ 331 processDragging_: function(e, eventLocation) { 332 if (!this.dragging_) 333 return true; 334 335 var index = -1; 336 for (var i = 0; i < this.displays_.length; i++) { 337 if (this.displays_[i] == this.dragging_.display) { 338 index = i; 339 break; 340 } 341 } 342 if (index < 0) 343 return true; 344 345 e.preventDefault(); 346 347 // Note that current code of moving display-rectangles doesn't work 348 // if there are >=3 displays. This is our assumption for M21. 349 // TODO(mukai): Fix the code to allow >=3 displays. 350 var newPosition = { 351 x: this.dragging_.originalLocation.x + 352 (eventLocation.x - this.dragging_.eventLocation.x), 353 y: this.dragging_.originalLocation.y + 354 (eventLocation.y - this.dragging_.eventLocation.y) 355 }; 356 357 var baseDiv = this.dragging_.display.isPrimary ? 358 this.secondaryDisplay_.div : this.primaryDisplay_.div; 359 var draggingDiv = this.dragging_.display.div; 360 361 newPosition.x = this.snapToEdge_(newPosition.x, draggingDiv.offsetWidth, 362 baseDiv.offsetLeft, baseDiv.offsetWidth); 363 newPosition.y = this.snapToEdge_(newPosition.y, draggingDiv.offsetHeight, 364 baseDiv.offsetTop, baseDiv.offsetHeight); 365 366 var newCenter = { 367 x: newPosition.x + draggingDiv.offsetWidth / 2, 368 y: newPosition.y + draggingDiv.offsetHeight / 2 369 }; 370 371 var baseBounds = { 372 x: baseDiv.offsetLeft, 373 y: baseDiv.offsetTop, 374 width: baseDiv.offsetWidth, 375 height: baseDiv.offsetHeight 376 }; 377 switch (getPositionToRectangle(baseBounds, newCenter)) { 378 case SecondaryDisplayLayout.RIGHT: 379 this.layout_ = this.dragging_.display.isPrimary ? 380 SecondaryDisplayLayout.LEFT : SecondaryDisplayLayout.RIGHT; 381 break; 382 case SecondaryDisplayLayout.LEFT: 383 this.layout_ = this.dragging_.display.isPrimary ? 384 SecondaryDisplayLayout.RIGHT : SecondaryDisplayLayout.LEFT; 385 break; 386 case SecondaryDisplayLayout.TOP: 387 this.layout_ = this.dragging_.display.isPrimary ? 388 SecondaryDisplayLayout.BOTTOM : SecondaryDisplayLayout.TOP; 389 break; 390 case SecondaryDisplayLayout.BOTTOM: 391 this.layout_ = this.dragging_.display.isPrimary ? 392 SecondaryDisplayLayout.TOP : SecondaryDisplayLayout.BOTTOM; 393 break; 394 } 395 396 if (this.layout_ == SecondaryDisplayLayout.LEFT || 397 this.layout_ == SecondaryDisplayLayout.RIGHT) { 398 if (newPosition.y > baseDiv.offsetTop + baseDiv.offsetHeight) 399 this.layout_ = this.dragging_.display.isPrimary ? 400 SecondaryDisplayLayout.TOP : SecondaryDisplayLayout.BOTTOM; 401 else if (newPosition.y + draggingDiv.offsetHeight < 402 baseDiv.offsetTop) 403 this.layout_ = this.dragging_.display.isPrimary ? 404 SecondaryDisplayLayout.BOTTOM : SecondaryDisplayLayout.TOP; 405 } else { 406 if (newPosition.x > baseDiv.offsetLeft + baseDiv.offsetWidth) 407 this.layout_ = this.dragging_.display.isPrimary ? 408 SecondaryDisplayLayout.LEFT : SecondaryDisplayLayout.RIGHT; 409 else if (newPosition.x + draggingDiv.offsetWidth < 410 baseDiv.offstLeft) 411 this.layout_ = this.dragging_.display.isPrimary ? 412 SecondaryDisplayLayout.RIGHT : SecondaryDisplayLayout.LEFT; 413 } 414 415 var layoutToBase; 416 if (!this.dragging_.display.isPrimary) { 417 layoutToBase = this.layout_; 418 } else { 419 switch (this.layout_) { 420 case SecondaryDisplayLayout.RIGHT: 421 layoutToBase = SecondaryDisplayLayout.LEFT; 422 break; 423 case SecondaryDisplayLayout.LEFT: 424 layoutToBase = SecondaryDisplayLayout.RIGHT; 425 break; 426 case SecondaryDisplayLayout.TOP: 427 layoutToBase = SecondaryDisplayLayout.BOTTOM; 428 break; 429 case SecondaryDisplayLayout.BOTTOM: 430 layoutToBase = SecondaryDisplayLayout.TOP; 431 break; 432 } 433 } 434 435 switch (layoutToBase) { 436 case SecondaryDisplayLayout.RIGHT: 437 draggingDiv.style.left = 438 baseDiv.offsetLeft + baseDiv.offsetWidth + 'px'; 439 draggingDiv.style.top = newPosition.y + 'px'; 440 break; 441 case SecondaryDisplayLayout.LEFT: 442 draggingDiv.style.left = 443 baseDiv.offsetLeft - draggingDiv.offsetWidth + 'px'; 444 draggingDiv.style.top = newPosition.y + 'px'; 445 break; 446 case SecondaryDisplayLayout.TOP: 447 draggingDiv.style.top = 448 baseDiv.offsetTop - draggingDiv.offsetHeight + 'px'; 449 draggingDiv.style.left = newPosition.x + 'px'; 450 break; 451 case SecondaryDisplayLayout.BOTTOM: 452 draggingDiv.style.top = 453 baseDiv.offsetTop + baseDiv.offsetHeight + 'px'; 454 draggingDiv.style.left = newPosition.x + 'px'; 455 break; 456 } 457 458 return false; 459 }, 460 461 /** 462 * start dragging of a display rectangle. 463 * @param {HTMLElement} target The event target. 464 * @param {Object} eventLocation The object to hold the location where 465 * this event happens. 466 * @private 467 */ 468 startDragging_: function(target, eventLocation) { 469 this.focusedIndex_ = null; 470 for (var i = 0; i < this.displays_.length; i++) { 471 var display = this.displays_[i]; 472 if (display.div == target || 473 (target.offsetParent && target.offsetParent == display.div)) { 474 this.focusedIndex_ = i; 475 break; 476 } 477 } 478 479 for (var i = 0; i < this.displays_.length; i++) { 480 var display = this.displays_[i]; 481 display.div.className = 'displays-display'; 482 if (i != this.focusedIndex_) 483 continue; 484 485 display.div.classList.add('displays-focused'); 486 if (this.displays_.length > 1) { 487 this.dragging_ = { 488 display: display, 489 originalLocation: { 490 x: display.div.offsetLeft, y: display.div.offsetTop 491 }, 492 eventLocation: eventLocation 493 }; 494 } 495 } 496 497 this.updateSelectedDisplayDescription_(); 498 return false; 499 }, 500 501 /** 502 * finish the current dragging of displays. 503 * @param {Event} e The event which triggers this. 504 * @private 505 */ 506 endDragging_: function(e) { 507 this.lastTouchLocation_ = null; 508 if (this.dragging_) { 509 // Make sure the dragging location is connected. 510 var baseDiv = this.dragging_.display.isPrimary ? 511 this.secondaryDisplay_.div : this.primaryDisplay_.div; 512 var draggingDiv = this.dragging_.display.div; 513 if (this.layout_ == SecondaryDisplayLayout.LEFT || 514 this.layout_ == SecondaryDisplayLayout.RIGHT) { 515 var top = Math.max(draggingDiv.offsetTop, 516 baseDiv.offsetTop - draggingDiv.offsetHeight + 517 MIN_OFFSET_OVERLAP); 518 top = Math.min(top, 519 baseDiv.offsetTop + baseDiv.offsetHeight - 520 MIN_OFFSET_OVERLAP); 521 draggingDiv.style.top = top + 'px'; 522 } else { 523 var left = Math.max(draggingDiv.offsetLeft, 524 baseDiv.offsetLeft - draggingDiv.offsetWidth + 525 MIN_OFFSET_OVERLAP); 526 left = Math.min(left, 527 baseDiv.offsetLeft + baseDiv.offsetWidth - 528 MIN_OFFSET_OVERLAP); 529 draggingDiv.style.left = left + 'px'; 530 } 531 var originalPosition = this.dragging_.display.originalPosition; 532 if (originalPosition.x != draggingDiv.offsetLeft || 533 originalPosition.y != draggingDiv.offsetTop) 534 this.applyResult_(); 535 this.dragging_ = null; 536 } 537 this.updateSelectedDisplayDescription_(); 538 return false; 539 }, 540 541 /** 542 * Updates the description of selected display section for mirroring mode. 543 * @private 544 */ 545 updateSelectedDisplaySectionMirroring_: function() { 546 $('display-configuration-arrow').hidden = true; 547 $('display-options-set-primary').disabled = true; 548 $('display-options-toggle-mirroring').disabled = false; 549 $('selected-display-start-calibrating-overscan').disabled = true; 550 $('display-options-orientation-selection').disabled = true; 551 var display = this.displays_[0]; 552 $('selected-display-name').textContent = 553 loadTimeData.getString('mirroringDisplay'); 554 var resolution = $('display-options-resolution-selection'); 555 var option = document.createElement('option'); 556 option.value = 'default'; 557 option.textContent = display.width + 'x' + display.height; 558 resolution.appendChild(option); 559 resolution.disabled = true; 560 }, 561 562 /** 563 * Updates the description of selected display section when no display is 564 * selected. 565 * @private 566 */ 567 updateSelectedDisplaySectionNoSelected_: function() { 568 $('display-configuration-arrow').hidden = true; 569 $('display-options-set-primary').disabled = true; 570 $('display-options-toggle-mirroring').disabled = true; 571 $('selected-display-start-calibrating-overscan').disabled = true; 572 $('display-options-orientation-selection').disabled = true; 573 $('selected-display-name').textContent = ''; 574 var resolution = $('display-options-resolution-selection'); 575 resolution.appendChild(document.createElement('option')); 576 resolution.disabled = true; 577 }, 578 579 /** 580 * Updates the description of selected display section for the selected 581 * display. 582 * @param {Object} display The selected display object. 583 * @private 584 */ 585 updateSelectedDisplaySectionForDisplay_: function(display) { 586 var arrow = $('display-configuration-arrow'); 587 arrow.hidden = false; 588 // Adding 1 px to the position to fit the border line and the border in 589 // arrow precisely. 590 arrow.style.top = $('display-configurations').offsetTop - 591 arrow.offsetHeight / 2 + 'px'; 592 arrow.style.left = display.div.offsetLeft + 593 display.div.offsetWidth / 2 - arrow.offsetWidth / 2 + 'px'; 594 595 $('display-options-set-primary').disabled = display.isPrimary; 596 $('display-options-toggle-mirroring').disabled = 597 (this.displays_.length <= 1); 598 $('selected-display-start-calibrating-overscan').disabled = 599 display.isInternal; 600 601 var orientation = $('display-options-orientation-selection'); 602 orientation.disabled = false; 603 var orientationOptions = orientation.getElementsByTagName('option'); 604 orientationOptions[display.orientation].selected = true; 605 606 $('selected-display-name').textContent = display.name; 607 608 var resolution = $('display-options-resolution-selection'); 609 if (display.resolutions.length <= 1) { 610 var option = document.createElement('option'); 611 option.value = 'default'; 612 option.textContent = display.width + 'x' + display.height; 613 option.selected = true; 614 resolution.appendChild(option); 615 resolution.disabled = true; 616 } else { 617 for (var i = 0; i < display.resolutions.length; i++) { 618 var option = document.createElement('option'); 619 option.value = i; 620 option.textContent = display.resolutions[i].width + 'x' + 621 display.resolutions[i].height; 622 if (display.resolutions[i].isBest) { 623 option.textContent += ' ' + 624 loadTimeData.getString('annotateBest'); 625 } 626 option.selected = display.resolutions[i].selected; 627 resolution.appendChild(option); 628 } 629 resolution.disabled = (display.resolutions.length <= 1); 630 } 631 }, 632 633 /** 634 * Updates the description of the selected display section. 635 * @private 636 */ 637 updateSelectedDisplayDescription_: function() { 638 var resolution = $('display-options-resolution-selection'); 639 resolution.textContent = ''; 640 var orientation = $('display-options-orientation-selection'); 641 var orientationOptions = orientation.getElementsByTagName('option'); 642 for (var i = 0; i < orientationOptions.length; i++) 643 orientationOptions.selected = false; 644 645 if (this.mirroring_) { 646 this.updateSelectedDisplaySectionMirroring_(); 647 } else if (this.focusedIndex_ == null || 648 this.displays_[this.focusedIndex_] == null) { 649 this.updateSelectedDisplaySectionNoSelected_(); 650 } else { 651 this.updateSelectedDisplaySectionForDisplay_( 652 this.displays_[this.focusedIndex_]); 653 } 654 }, 655 656 /** 657 * Clears the drawing area for display rectangles. 658 * @private 659 */ 660 resetDisplaysView_: function() { 661 var displaysViewHost = $('display-options-displays-view-host'); 662 displaysViewHost.removeChild(displaysViewHost.firstChild); 663 this.displaysView_ = document.createElement('div'); 664 this.displaysView_.id = 'display-options-displays-view'; 665 displaysViewHost.appendChild(this.displaysView_); 666 }, 667 668 /** 669 * Lays out the display rectangles for mirroring. 670 * @private 671 */ 672 layoutMirroringDisplays_: function() { 673 // Offset pixels for secondary display rectangles. The offset includes the 674 // border width. 675 /** @const */ var MIRRORING_OFFSET_PIXELS = 3; 676 // Always show two displays because there must be two displays when 677 // the display_options is enabled. Don't rely on displays_.length because 678 // there is only one display from chrome's perspective in mirror mode. 679 /** @const */ var MIN_NUM_DISPLAYS = 2; 680 /** @const */ var MIRRORING_VERTICAL_MARGIN = 20; 681 682 // The width/height should be same as the first display: 683 var width = Math.ceil(this.displays_[0].width * this.visualScale_); 684 var height = Math.ceil(this.displays_[0].height * this.visualScale_); 685 686 var numDisplays = Math.max(MIN_NUM_DISPLAYS, this.displays_.length); 687 688 var totalWidth = width + numDisplays * MIRRORING_OFFSET_PIXELS; 689 var totalHeight = height + numDisplays * MIRRORING_OFFSET_PIXELS; 690 691 this.displaysView_.style.height = totalHeight + 'px'; 692 this.displaysView_.classList.add( 693 'display-options-displays-view-mirroring'); 694 695 // The displays should be centered. 696 var offsetX = 697 $('display-options-displays-view').offsetWidth / 2 - totalWidth / 2; 698 699 for (var i = 0; i < numDisplays; i++) { 700 var div = document.createElement('div'); 701 div.className = 'displays-display'; 702 div.style.top = i * MIRRORING_OFFSET_PIXELS + 'px'; 703 div.style.left = i * MIRRORING_OFFSET_PIXELS + offsetX + 'px'; 704 div.style.width = width + 'px'; 705 div.style.height = height + 'px'; 706 div.style.zIndex = i; 707 // set 'display-mirrored' class for the background display rectangles. 708 if (i != numDisplays - 1) 709 div.classList.add('display-mirrored'); 710 this.displaysView_.appendChild(div); 711 } 712 }, 713 714 /** 715 * Layouts the display rectangles according to the current layout_. 716 * @private 717 */ 718 layoutDisplays_: function() { 719 var maxWidth = 0; 720 var maxHeight = 0; 721 var boundingBox = {left: 0, right: 0, top: 0, bottom: 0}; 722 for (var i = 0; i < this.displays_.length; i++) { 723 var display = this.displays_[i]; 724 boundingBox.left = Math.min(boundingBox.left, display.x); 725 boundingBox.right = Math.max( 726 boundingBox.right, display.x + display.width); 727 boundingBox.top = Math.min(boundingBox.top, display.y); 728 boundingBox.bottom = Math.max( 729 boundingBox.bottom, display.y + display.height); 730 maxWidth = Math.max(maxWidth, display.width); 731 maxHeight = Math.max(maxHeight, display.height); 732 } 733 734 // Make the margin around the bounding box. 735 var areaWidth = boundingBox.right - boundingBox.left + maxWidth; 736 var areaHeight = boundingBox.bottom - boundingBox.top + maxHeight; 737 738 // Calculates the scale by the width since horizontal size is more strict. 739 // TODO(mukai): Adds the check of vertical size in case. 740 this.visualScale_ = Math.min( 741 VISUAL_SCALE, this.displaysView_.offsetWidth / areaWidth); 742 743 // Prepare enough area for thisplays_view by adding the maximum height. 744 this.displaysView_.style.height = 745 Math.ceil(areaHeight * this.visualScale_) + 'px'; 746 747 var boundingCenter = { 748 x: Math.floor((boundingBox.right + boundingBox.left) * 749 this.visualScale_ / 2), 750 y: Math.floor((boundingBox.bottom + boundingBox.top) * 751 this.visualScale_ / 2) 752 }; 753 754 // Centering the bounding box of the display rectangles. 755 var offset = { 756 x: Math.floor(this.displaysView_.offsetWidth / 2 - 757 (boundingBox.right + boundingBox.left) * this.visualScale_ / 2), 758 y: Math.floor(this.displaysView_.offsetHeight / 2 - 759 (boundingBox.bottom + boundingBox.top) * this.visualScale_ / 2) 760 }; 761 762 for (var i = 0; i < this.displays_.length; i++) { 763 var display = this.displays_[i]; 764 var div = document.createElement('div'); 765 display.div = div; 766 767 div.className = 'displays-display'; 768 if (i == this.focusedIndex_) 769 div.classList.add('displays-focused'); 770 771 if (display.isPrimary) { 772 this.primaryDisplay_ = display; 773 } else { 774 this.secondaryDisplay_ = display; 775 } 776 var displayNameContainer = document.createElement('div'); 777 displayNameContainer.textContent = display.name; 778 div.appendChild(displayNameContainer); 779 display.nameContainer = displayNameContainer; 780 display.div.style.width = 781 Math.floor(display.width * this.visualScale_) + 'px'; 782 var newHeight = Math.floor(display.height * this.visualScale_); 783 display.div.style.height = newHeight + 'px'; 784 div.style.left = 785 Math.floor(display.x * this.visualScale_) + offset.x + 'px'; 786 div.style.top = 787 Math.floor(display.y * this.visualScale_) + offset.y + 'px'; 788 display.nameContainer.style.marginTop = 789 (newHeight - display.nameContainer.offsetHeight) / 2 + 'px'; 790 791 div.onmousedown = this.onMouseDown_.bind(this); 792 div.ontouchstart = this.onTouchStart_.bind(this); 793 794 this.displaysView_.appendChild(div); 795 796 // Set the margin top to place the display name at the middle of the 797 // rectangle. Note that this has to be done after it's added into the 798 // |displaysView_|. Otherwise its offsetHeight is yet 0. 799 displayNameContainer.style.marginTop = 800 (div.offsetHeight - displayNameContainer.offsetHeight) / 2 + 'px'; 801 display.originalPosition = {x: div.offsetLeft, y: div.offsetTop}; 802 } 803 }, 804 805 /** 806 * Called when the display arrangement has changed. 807 * @param {boolean} mirroring Whether current mode is mirroring or not. 808 * @param {Array} displays The list of the display information. 809 * @param {SecondaryDisplayLayout} layout The layout strategy. 810 * @param {number} offset The offset of the secondary display. 811 * @private 812 */ 813 onDisplayChanged_: function(mirroring, displays, layout, offset) { 814 if (!this.visible) 815 return; 816 817 var hasExternal = false; 818 for (var i = 0; i < displays.length; i++) { 819 if (!displays[i].isInternal) { 820 hasExternal = true; 821 break; 822 } 823 } 824 825 this.layout_ = layout; 826 827 $('display-options-toggle-mirroring').textContent = 828 loadTimeData.getString( 829 mirroring ? 'stopMirroring' : 'startMirroring'); 830 831 // Focus to the first display next to the primary one when |displays| list 832 // is updated. 833 if (mirroring) { 834 this.focusedIndex_ = null; 835 } else if (this.mirroring_ != mirroring || 836 this.displays_.length != displays.length) { 837 this.focusedIndex_ = 0; 838 } 839 840 this.mirroring_ = mirroring; 841 this.displays_ = displays; 842 843 this.resetDisplaysView_(); 844 if (this.mirroring_) 845 this.layoutMirroringDisplays_(); 846 else 847 this.layoutDisplays_(); 848 849 this.updateSelectedDisplayDescription_(); 850 } 851 }; 852 853 DisplayOptions.setDisplayInfo = function( 854 mirroring, displays, layout, offset) { 855 DisplayOptions.getInstance().onDisplayChanged_( 856 mirroring, displays, layout, offset); 857 }; 858 859 // Export 860 return { 861 DisplayOptions: DisplayOptions 862 }; 863 }); 864