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