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