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   // 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