Home | History | Annotate | Download | only in chromeos
      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