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 $('display-options-color-profile-selection').onchange = function(ev) { 182 chrome.send('setColorProfile', [this.displays_[this.focusedIndex_].id, 183 ev.target.value]); 184 }.bind(this); 185 $('selected-display-start-calibrating-overscan').onclick = function() { 186 // Passes the target display ID. Do not specify it through URL hash, 187 // we do not care back/forward. 188 var displayOverscan = options.DisplayOverscan.getInstance(); 189 displayOverscan.setDisplayId(this.displays_[this.focusedIndex_].id); 190 OptionsPage.navigateToPage('displayOverscan'); 191 chrome.send('coreOptionsUserMetricsAction', 192 ['Options_DisplaySetOverscan']); 193 }.bind(this); 194 195 chrome.send('getDisplayInfo'); 196 }, 197 198 /** @override */ 199 didShowPage: function() { 200 var optionTitles = document.getElementsByClassName( 201 'selected-display-option-title'); 202 var maxSize = 0; 203 for (var i = 0; i < optionTitles.length; i++) 204 maxSize = Math.max(maxSize, optionTitles[i].clientWidth); 205 for (var i = 0; i < optionTitles.length; i++) 206 optionTitles[i].style.width = maxSize + 'px'; 207 }, 208 209 /** @override */ 210 onVisibilityChanged_: function() { 211 OptionsPage.prototype.onVisibilityChanged_(this); 212 if (this.visible) 213 chrome.send('getDisplayInfo'); 214 }, 215 216 /** 217 * Mouse move handler for dragging display rectangle. 218 * @param {Event} e The mouse move event. 219 * @private 220 */ 221 onMouseMove_: function(e) { 222 return this.processDragging_(e, {x: e.pageX, y: e.pageY}); 223 }, 224 225 /** 226 * Touch move handler for dragging display rectangle. 227 * @param {Event} e The touch move event. 228 * @private 229 */ 230 onTouchMove_: function(e) { 231 if (e.touches.length != 1) 232 return true; 233 234 var touchLocation = {x: e.touches[0].pageX, y: e.touches[0].pageY}; 235 // Touch move events happen even if the touch location doesn't change, but 236 // it doesn't need to process the dragging. Since sometimes the touch 237 // position changes slightly even though the user doesn't think to move 238 // the finger, very small move is just ignored. 239 /** @const */ var IGNORABLE_TOUCH_MOVE_PX = 1; 240 var xDiff = Math.abs(touchLocation.x - this.lastTouchLocation_.x); 241 var yDiff = Math.abs(touchLocation.y - this.lastTouchLocation_.y); 242 if (xDiff <= IGNORABLE_TOUCH_MOVE_PX && 243 yDiff <= IGNORABLE_TOUCH_MOVE_PX) { 244 return true; 245 } 246 247 this.lastTouchLocation_ = touchLocation; 248 return this.processDragging_(e, touchLocation); 249 }, 250 251 /** 252 * Mouse down handler for dragging display rectangle. 253 * @param {Event} e The mouse down event. 254 * @private 255 */ 256 onMouseDown_: function(e) { 257 if (this.mirroring_) 258 return true; 259 260 if (e.button != 0) 261 return true; 262 263 e.preventDefault(); 264 return this.startDragging_(e.target, {x: e.pageX, y: e.pageY}); 265 }, 266 267 /** 268 * Touch start handler for dragging display rectangle. 269 * @param {Event} e The touch start event. 270 * @private 271 */ 272 onTouchStart_: function(e) { 273 if (this.mirroring_) 274 return true; 275 276 if (e.touches.length != 1) 277 return false; 278 279 e.preventDefault(); 280 var touch = e.touches[0]; 281 this.lastTouchLocation_ = {x: touch.pageX, y: touch.pageY}; 282 return this.startDragging_(e.target, this.lastTouchLocation_); 283 }, 284 285 /** 286 * Collects the current data and sends it to Chrome. 287 * @private 288 */ 289 applyResult_: function() { 290 // Offset is calculated from top or left edge. 291 var primary = this.primaryDisplay_; 292 var secondary = this.secondaryDisplay_; 293 var offset; 294 if (this.layout_ == SecondaryDisplayLayout.LEFT || 295 this.layout_ == SecondaryDisplayLayout.RIGHT) { 296 offset = secondary.div.offsetTop - primary.div.offsetTop; 297 } else { 298 offset = secondary.div.offsetLeft - primary.div.offsetLeft; 299 } 300 chrome.send('setDisplayLayout', 301 [this.layout_, offset / this.visualScale_]); 302 }, 303 304 /** 305 * Snaps the region [point, width] to [basePoint, baseWidth] if 306 * the [point, width] is close enough to the base's edge. 307 * @param {number} point The starting point of the region. 308 * @param {number} width The width of the region. 309 * @param {number} basePoint The starting point of the base region. 310 * @param {number} baseWidth The width of the base region. 311 * @return {number} The moved point. Returns point itself if it doesn't 312 * need to snap to the edge. 313 * @private 314 */ 315 snapToEdge_: function(point, width, basePoint, baseWidth) { 316 // If the edge of the regions is smaller than this, it will snap to the 317 // base's edge. 318 /** @const */ var SNAP_DISTANCE_PX = 16; 319 320 var startDiff = Math.abs(point - basePoint); 321 var endDiff = Math.abs(point + width - (basePoint + baseWidth)); 322 // Prefer the closer one if both edges are close enough. 323 if (startDiff < SNAP_DISTANCE_PX && startDiff < endDiff) 324 return basePoint; 325 else if (endDiff < SNAP_DISTANCE_PX) 326 return basePoint + baseWidth - width; 327 328 return point; 329 }, 330 331 /** 332 * Processes the actual dragging of display rectangle. 333 * @param {Event} e The event which triggers this drag. 334 * @param {Object} eventLocation The location where the event happens. 335 * @private 336 */ 337 processDragging_: function(e, eventLocation) { 338 if (!this.dragging_) 339 return true; 340 341 var index = -1; 342 for (var i = 0; i < this.displays_.length; i++) { 343 if (this.displays_[i] == this.dragging_.display) { 344 index = i; 345 break; 346 } 347 } 348 if (index < 0) 349 return true; 350 351 e.preventDefault(); 352 353 // Note that current code of moving display-rectangles doesn't work 354 // if there are >=3 displays. This is our assumption for M21. 355 // TODO(mukai): Fix the code to allow >=3 displays. 356 var newPosition = { 357 x: this.dragging_.originalLocation.x + 358 (eventLocation.x - this.dragging_.eventLocation.x), 359 y: this.dragging_.originalLocation.y + 360 (eventLocation.y - this.dragging_.eventLocation.y) 361 }; 362 363 var baseDiv = this.dragging_.display.isPrimary ? 364 this.secondaryDisplay_.div : this.primaryDisplay_.div; 365 var draggingDiv = this.dragging_.display.div; 366 367 newPosition.x = this.snapToEdge_(newPosition.x, draggingDiv.offsetWidth, 368 baseDiv.offsetLeft, baseDiv.offsetWidth); 369 newPosition.y = this.snapToEdge_(newPosition.y, draggingDiv.offsetHeight, 370 baseDiv.offsetTop, baseDiv.offsetHeight); 371 372 var newCenter = { 373 x: newPosition.x + draggingDiv.offsetWidth / 2, 374 y: newPosition.y + draggingDiv.offsetHeight / 2 375 }; 376 377 var baseBounds = { 378 x: baseDiv.offsetLeft, 379 y: baseDiv.offsetTop, 380 width: baseDiv.offsetWidth, 381 height: baseDiv.offsetHeight 382 }; 383 switch (getPositionToRectangle(baseBounds, newCenter)) { 384 case SecondaryDisplayLayout.RIGHT: 385 this.layout_ = this.dragging_.display.isPrimary ? 386 SecondaryDisplayLayout.LEFT : SecondaryDisplayLayout.RIGHT; 387 break; 388 case SecondaryDisplayLayout.LEFT: 389 this.layout_ = this.dragging_.display.isPrimary ? 390 SecondaryDisplayLayout.RIGHT : SecondaryDisplayLayout.LEFT; 391 break; 392 case SecondaryDisplayLayout.TOP: 393 this.layout_ = this.dragging_.display.isPrimary ? 394 SecondaryDisplayLayout.BOTTOM : SecondaryDisplayLayout.TOP; 395 break; 396 case SecondaryDisplayLayout.BOTTOM: 397 this.layout_ = this.dragging_.display.isPrimary ? 398 SecondaryDisplayLayout.TOP : SecondaryDisplayLayout.BOTTOM; 399 break; 400 } 401 402 if (this.layout_ == SecondaryDisplayLayout.LEFT || 403 this.layout_ == SecondaryDisplayLayout.RIGHT) { 404 if (newPosition.y > baseDiv.offsetTop + baseDiv.offsetHeight) 405 this.layout_ = this.dragging_.display.isPrimary ? 406 SecondaryDisplayLayout.TOP : SecondaryDisplayLayout.BOTTOM; 407 else if (newPosition.y + draggingDiv.offsetHeight < 408 baseDiv.offsetTop) 409 this.layout_ = this.dragging_.display.isPrimary ? 410 SecondaryDisplayLayout.BOTTOM : SecondaryDisplayLayout.TOP; 411 } else { 412 if (newPosition.x > baseDiv.offsetLeft + baseDiv.offsetWidth) 413 this.layout_ = this.dragging_.display.isPrimary ? 414 SecondaryDisplayLayout.LEFT : SecondaryDisplayLayout.RIGHT; 415 else if (newPosition.x + draggingDiv.offsetWidth < 416 baseDiv.offstLeft) 417 this.layout_ = this.dragging_.display.isPrimary ? 418 SecondaryDisplayLayout.RIGHT : SecondaryDisplayLayout.LEFT; 419 } 420 421 var layoutToBase; 422 if (!this.dragging_.display.isPrimary) { 423 layoutToBase = this.layout_; 424 } else { 425 switch (this.layout_) { 426 case SecondaryDisplayLayout.RIGHT: 427 layoutToBase = SecondaryDisplayLayout.LEFT; 428 break; 429 case SecondaryDisplayLayout.LEFT: 430 layoutToBase = SecondaryDisplayLayout.RIGHT; 431 break; 432 case SecondaryDisplayLayout.TOP: 433 layoutToBase = SecondaryDisplayLayout.BOTTOM; 434 break; 435 case SecondaryDisplayLayout.BOTTOM: 436 layoutToBase = SecondaryDisplayLayout.TOP; 437 break; 438 } 439 } 440 441 switch (layoutToBase) { 442 case SecondaryDisplayLayout.RIGHT: 443 draggingDiv.style.left = 444 baseDiv.offsetLeft + baseDiv.offsetWidth + 'px'; 445 draggingDiv.style.top = newPosition.y + 'px'; 446 break; 447 case SecondaryDisplayLayout.LEFT: 448 draggingDiv.style.left = 449 baseDiv.offsetLeft - draggingDiv.offsetWidth + 'px'; 450 draggingDiv.style.top = newPosition.y + 'px'; 451 break; 452 case SecondaryDisplayLayout.TOP: 453 draggingDiv.style.top = 454 baseDiv.offsetTop - draggingDiv.offsetHeight + 'px'; 455 draggingDiv.style.left = newPosition.x + 'px'; 456 break; 457 case SecondaryDisplayLayout.BOTTOM: 458 draggingDiv.style.top = 459 baseDiv.offsetTop + baseDiv.offsetHeight + 'px'; 460 draggingDiv.style.left = newPosition.x + 'px'; 461 break; 462 } 463 464 return false; 465 }, 466 467 /** 468 * start dragging of a display rectangle. 469 * @param {HTMLElement} target The event target. 470 * @param {Object} eventLocation The object to hold the location where 471 * this event happens. 472 * @private 473 */ 474 startDragging_: function(target, eventLocation) { 475 this.focusedIndex_ = null; 476 for (var i = 0; i < this.displays_.length; i++) { 477 var display = this.displays_[i]; 478 if (display.div == target || 479 (target.offsetParent && target.offsetParent == display.div)) { 480 this.focusedIndex_ = i; 481 break; 482 } 483 } 484 485 for (var i = 0; i < this.displays_.length; i++) { 486 var display = this.displays_[i]; 487 display.div.className = 'displays-display'; 488 if (i != this.focusedIndex_) 489 continue; 490 491 display.div.classList.add('displays-focused'); 492 if (this.displays_.length > 1) { 493 this.dragging_ = { 494 display: display, 495 originalLocation: { 496 x: display.div.offsetLeft, y: display.div.offsetTop 497 }, 498 eventLocation: eventLocation 499 }; 500 } 501 } 502 503 this.updateSelectedDisplayDescription_(); 504 return false; 505 }, 506 507 /** 508 * finish the current dragging of displays. 509 * @param {Event} e The event which triggers this. 510 * @private 511 */ 512 endDragging_: function(e) { 513 this.lastTouchLocation_ = null; 514 if (this.dragging_) { 515 // Make sure the dragging location is connected. 516 var baseDiv = this.dragging_.display.isPrimary ? 517 this.secondaryDisplay_.div : this.primaryDisplay_.div; 518 var draggingDiv = this.dragging_.display.div; 519 if (this.layout_ == SecondaryDisplayLayout.LEFT || 520 this.layout_ == SecondaryDisplayLayout.RIGHT) { 521 var top = Math.max(draggingDiv.offsetTop, 522 baseDiv.offsetTop - draggingDiv.offsetHeight + 523 MIN_OFFSET_OVERLAP); 524 top = Math.min(top, 525 baseDiv.offsetTop + baseDiv.offsetHeight - 526 MIN_OFFSET_OVERLAP); 527 draggingDiv.style.top = top + 'px'; 528 } else { 529 var left = Math.max(draggingDiv.offsetLeft, 530 baseDiv.offsetLeft - draggingDiv.offsetWidth + 531 MIN_OFFSET_OVERLAP); 532 left = Math.min(left, 533 baseDiv.offsetLeft + baseDiv.offsetWidth - 534 MIN_OFFSET_OVERLAP); 535 draggingDiv.style.left = left + 'px'; 536 } 537 var originalPosition = this.dragging_.display.originalPosition; 538 if (originalPosition.x != draggingDiv.offsetLeft || 539 originalPosition.y != draggingDiv.offsetTop) 540 this.applyResult_(); 541 this.dragging_ = null; 542 } 543 this.updateSelectedDisplayDescription_(); 544 return false; 545 }, 546 547 /** 548 * Updates the description of selected display section for mirroring mode. 549 * @private 550 */ 551 updateSelectedDisplaySectionMirroring_: function() { 552 $('display-configuration-arrow').hidden = true; 553 $('display-options-set-primary').disabled = true; 554 $('display-options-toggle-mirroring').disabled = false; 555 $('selected-display-start-calibrating-overscan').disabled = true; 556 $('display-options-orientation-selection').disabled = true; 557 var display = this.displays_[0]; 558 $('selected-display-name').textContent = 559 loadTimeData.getString('mirroringDisplay'); 560 var resolution = $('display-options-resolution-selection'); 561 var option = document.createElement('option'); 562 option.value = 'default'; 563 option.textContent = display.width + 'x' + display.height; 564 resolution.appendChild(option); 565 resolution.disabled = true; 566 }, 567 568 /** 569 * Updates the description of selected display section when no display is 570 * selected. 571 * @private 572 */ 573 updateSelectedDisplaySectionNoSelected_: function() { 574 $('display-configuration-arrow').hidden = true; 575 $('display-options-set-primary').disabled = true; 576 $('display-options-toggle-mirroring').disabled = true; 577 $('selected-display-start-calibrating-overscan').disabled = true; 578 $('display-options-orientation-selection').disabled = true; 579 $('selected-display-name').textContent = ''; 580 var resolution = $('display-options-resolution-selection'); 581 resolution.appendChild(document.createElement('option')); 582 resolution.disabled = true; 583 }, 584 585 /** 586 * Updates the description of selected display section for the selected 587 * display. 588 * @param {Object} display The selected display object. 589 * @private 590 */ 591 updateSelectedDisplaySectionForDisplay_: function(display) { 592 var arrow = $('display-configuration-arrow'); 593 arrow.hidden = false; 594 // Adding 1 px to the position to fit the border line and the border in 595 // arrow precisely. 596 arrow.style.top = $('display-configurations').offsetTop - 597 arrow.offsetHeight / 2 + 'px'; 598 arrow.style.left = display.div.offsetLeft + 599 display.div.offsetWidth / 2 - arrow.offsetWidth / 2 + 'px'; 600 601 $('display-options-set-primary').disabled = display.isPrimary; 602 $('display-options-toggle-mirroring').disabled = 603 (this.displays_.length <= 1); 604 $('selected-display-start-calibrating-overscan').disabled = 605 display.isInternal; 606 607 var orientation = $('display-options-orientation-selection'); 608 orientation.disabled = false; 609 var orientationOptions = orientation.getElementsByTagName('option'); 610 orientationOptions[display.orientation].selected = true; 611 612 $('selected-display-name').textContent = display.name; 613 614 var resolution = $('display-options-resolution-selection'); 615 if (display.resolutions.length <= 1) { 616 var option = document.createElement('option'); 617 option.value = 'default'; 618 option.textContent = display.width + 'x' + display.height; 619 option.selected = true; 620 resolution.appendChild(option); 621 resolution.disabled = true; 622 } else { 623 for (var i = 0; i < display.resolutions.length; i++) { 624 var option = document.createElement('option'); 625 option.value = i; 626 option.textContent = display.resolutions[i].width + 'x' + 627 display.resolutions[i].height; 628 if (display.resolutions[i].isBest) { 629 option.textContent += ' ' + 630 loadTimeData.getString('annotateBest'); 631 } 632 option.selected = display.resolutions[i].selected; 633 resolution.appendChild(option); 634 } 635 resolution.disabled = (display.resolutions.length <= 1); 636 } 637 638 if (display.availableColorProfiles.length <= 1) { 639 $('selected-display-color-profile-row').hidden = true; 640 } else { 641 $('selected-display-color-profile-row').hidden = false; 642 var profiles = $('display-options-color-profile-selection'); 643 profiles.innerHTML = ''; 644 for (var i = 0; i < display.availableColorProfiles.length; i++) { 645 var option = document.createElement('option'); 646 var colorProfile = display.availableColorProfiles[i]; 647 option.value = colorProfile.profileId; 648 option.textContent = colorProfile.name; 649 option.selected = ( 650 display.colorProfile == colorProfile.profileId); 651 profiles.appendChild(option); 652 } 653 } 654 }, 655 656 /** 657 * Updates the description of the selected display section. 658 * @private 659 */ 660 updateSelectedDisplayDescription_: function() { 661 var resolution = $('display-options-resolution-selection'); 662 resolution.textContent = ''; 663 var orientation = $('display-options-orientation-selection'); 664 var orientationOptions = orientation.getElementsByTagName('option'); 665 for (var i = 0; i < orientationOptions.length; i++) 666 orientationOptions.selected = false; 667 668 if (this.mirroring_) { 669 this.updateSelectedDisplaySectionMirroring_(); 670 } else if (this.focusedIndex_ == null || 671 this.displays_[this.focusedIndex_] == null) { 672 this.updateSelectedDisplaySectionNoSelected_(); 673 } else { 674 this.updateSelectedDisplaySectionForDisplay_( 675 this.displays_[this.focusedIndex_]); 676 } 677 }, 678 679 /** 680 * Clears the drawing area for display rectangles. 681 * @private 682 */ 683 resetDisplaysView_: function() { 684 var displaysViewHost = $('display-options-displays-view-host'); 685 displaysViewHost.removeChild(displaysViewHost.firstChild); 686 this.displaysView_ = document.createElement('div'); 687 this.displaysView_.id = 'display-options-displays-view'; 688 displaysViewHost.appendChild(this.displaysView_); 689 }, 690 691 /** 692 * Lays out the display rectangles for mirroring. 693 * @private 694 */ 695 layoutMirroringDisplays_: function() { 696 // Offset pixels for secondary display rectangles. The offset includes the 697 // border width. 698 /** @const */ var MIRRORING_OFFSET_PIXELS = 3; 699 // Always show two displays because there must be two displays when 700 // the display_options is enabled. Don't rely on displays_.length because 701 // there is only one display from chrome's perspective in mirror mode. 702 /** @const */ var MIN_NUM_DISPLAYS = 2; 703 /** @const */ var MIRRORING_VERTICAL_MARGIN = 20; 704 705 // The width/height should be same as the first display: 706 var width = Math.ceil(this.displays_[0].width * this.visualScale_); 707 var height = Math.ceil(this.displays_[0].height * this.visualScale_); 708 709 var numDisplays = Math.max(MIN_NUM_DISPLAYS, this.displays_.length); 710 711 var totalWidth = width + numDisplays * MIRRORING_OFFSET_PIXELS; 712 var totalHeight = height + numDisplays * MIRRORING_OFFSET_PIXELS; 713 714 this.displaysView_.style.height = totalHeight + 'px'; 715 this.displaysView_.classList.add( 716 'display-options-displays-view-mirroring'); 717 718 // The displays should be centered. 719 var offsetX = 720 $('display-options-displays-view').offsetWidth / 2 - totalWidth / 2; 721 722 for (var i = 0; i < numDisplays; i++) { 723 var div = document.createElement('div'); 724 div.className = 'displays-display'; 725 div.style.top = i * MIRRORING_OFFSET_PIXELS + 'px'; 726 div.style.left = i * MIRRORING_OFFSET_PIXELS + offsetX + 'px'; 727 div.style.width = width + 'px'; 728 div.style.height = height + 'px'; 729 div.style.zIndex = i; 730 // set 'display-mirrored' class for the background display rectangles. 731 if (i != numDisplays - 1) 732 div.classList.add('display-mirrored'); 733 this.displaysView_.appendChild(div); 734 } 735 }, 736 737 /** 738 * Layouts the display rectangles according to the current layout_. 739 * @private 740 */ 741 layoutDisplays_: function() { 742 var maxWidth = 0; 743 var maxHeight = 0; 744 var boundingBox = {left: 0, right: 0, top: 0, bottom: 0}; 745 for (var i = 0; i < this.displays_.length; i++) { 746 var display = this.displays_[i]; 747 boundingBox.left = Math.min(boundingBox.left, display.x); 748 boundingBox.right = Math.max( 749 boundingBox.right, display.x + display.width); 750 boundingBox.top = Math.min(boundingBox.top, display.y); 751 boundingBox.bottom = Math.max( 752 boundingBox.bottom, display.y + display.height); 753 maxWidth = Math.max(maxWidth, display.width); 754 maxHeight = Math.max(maxHeight, display.height); 755 } 756 757 // Make the margin around the bounding box. 758 var areaWidth = boundingBox.right - boundingBox.left + maxWidth; 759 var areaHeight = boundingBox.bottom - boundingBox.top + maxHeight; 760 761 // Calculates the scale by the width since horizontal size is more strict. 762 // TODO(mukai): Adds the check of vertical size in case. 763 this.visualScale_ = Math.min( 764 VISUAL_SCALE, this.displaysView_.offsetWidth / areaWidth); 765 766 // Prepare enough area for thisplays_view by adding the maximum height. 767 this.displaysView_.style.height = 768 Math.ceil(areaHeight * this.visualScale_) + 'px'; 769 770 var boundingCenter = { 771 x: Math.floor((boundingBox.right + boundingBox.left) * 772 this.visualScale_ / 2), 773 y: Math.floor((boundingBox.bottom + boundingBox.top) * 774 this.visualScale_ / 2) 775 }; 776 777 // Centering the bounding box of the display rectangles. 778 var offset = { 779 x: Math.floor(this.displaysView_.offsetWidth / 2 - 780 (boundingBox.right + boundingBox.left) * this.visualScale_ / 2), 781 y: Math.floor(this.displaysView_.offsetHeight / 2 - 782 (boundingBox.bottom + boundingBox.top) * this.visualScale_ / 2) 783 }; 784 785 for (var i = 0; i < this.displays_.length; i++) { 786 var display = this.displays_[i]; 787 var div = document.createElement('div'); 788 display.div = div; 789 790 div.className = 'displays-display'; 791 if (i == this.focusedIndex_) 792 div.classList.add('displays-focused'); 793 794 if (display.isPrimary) { 795 this.primaryDisplay_ = display; 796 } else { 797 this.secondaryDisplay_ = display; 798 } 799 var displayNameContainer = document.createElement('div'); 800 displayNameContainer.textContent = display.name; 801 div.appendChild(displayNameContainer); 802 display.nameContainer = displayNameContainer; 803 display.div.style.width = 804 Math.floor(display.width * this.visualScale_) + 'px'; 805 var newHeight = Math.floor(display.height * this.visualScale_); 806 display.div.style.height = newHeight + 'px'; 807 div.style.left = 808 Math.floor(display.x * this.visualScale_) + offset.x + 'px'; 809 div.style.top = 810 Math.floor(display.y * this.visualScale_) + offset.y + 'px'; 811 display.nameContainer.style.marginTop = 812 (newHeight - display.nameContainer.offsetHeight) / 2 + 'px'; 813 814 div.onmousedown = this.onMouseDown_.bind(this); 815 div.ontouchstart = this.onTouchStart_.bind(this); 816 817 this.displaysView_.appendChild(div); 818 819 // Set the margin top to place the display name at the middle of the 820 // rectangle. Note that this has to be done after it's added into the 821 // |displaysView_|. Otherwise its offsetHeight is yet 0. 822 displayNameContainer.style.marginTop = 823 (div.offsetHeight - displayNameContainer.offsetHeight) / 2 + 'px'; 824 display.originalPosition = {x: div.offsetLeft, y: div.offsetTop}; 825 } 826 }, 827 828 /** 829 * Called when the display arrangement has changed. 830 * @param {boolean} mirroring Whether current mode is mirroring or not. 831 * @param {Array} displays The list of the display information. 832 * @param {SecondaryDisplayLayout} layout The layout strategy. 833 * @param {number} offset The offset of the secondary display. 834 * @private 835 */ 836 onDisplayChanged_: function(mirroring, displays, layout, offset) { 837 if (!this.visible) 838 return; 839 840 var hasExternal = false; 841 for (var i = 0; i < displays.length; i++) { 842 if (!displays[i].isInternal) { 843 hasExternal = true; 844 break; 845 } 846 } 847 848 this.layout_ = layout; 849 850 $('display-options-toggle-mirroring').textContent = 851 loadTimeData.getString( 852 mirroring ? 'stopMirroring' : 'startMirroring'); 853 854 // Focus to the first display next to the primary one when |displays| list 855 // is updated. 856 if (mirroring) { 857 this.focusedIndex_ = null; 858 } else if (this.mirroring_ != mirroring || 859 this.displays_.length != displays.length) { 860 this.focusedIndex_ = 0; 861 } 862 863 this.mirroring_ = mirroring; 864 this.displays_ = displays; 865 866 this.resetDisplaysView_(); 867 if (this.mirroring_) 868 this.layoutMirroringDisplays_(); 869 else 870 this.layoutDisplays_(); 871 872 this.updateSelectedDisplayDescription_(); 873 } 874 }; 875 876 DisplayOptions.setDisplayInfo = function( 877 mirroring, displays, layout, offset) { 878 DisplayOptions.getInstance().onDisplayChanged_( 879 mirroring, displays, layout, offset); 880 }; 881 882 // Export 883 return { 884 DisplayOptions: DisplayOptions 885 }; 886 }); 887