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