Disable view source for Developer Tools.
[chromium-blink-merge.git] / chrome / browser / resources / options / chromeos / display_options.js
blob3b000ca283ee6deb4605e59fd59aa495fcdcd810
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.
5 cr.define('options', function() {
6   var OptionsPage = options.OptionsPage;
8   // The scale ratio of the display rectangle to its original size.
9   /** @const */ var VISUAL_SCALE = 1 / 10;
11   // The number of pixels to share the edges between displays.
12   /** @const */ var MIN_OFFSET_OVERLAP = 5;
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   };
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   }
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;
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   }
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   }
83   cr.addSingletonGetter(DisplayOptions);
85   DisplayOptions.prototype = {
86     __proto__: OptionsPage.prototype,
88     /**
89      * Whether the current output status is mirroring displays or not.
90      * @private
91      */
92     mirroring_: false,
94     /**
95      * The current secondary display layout.
96      * @private
97      */
98     layout_: SecondaryDisplayLayout.RIGHT,
100     /**
101      * The array of current output displays.  It also contains the display
102      * rectangles currently rendered on screen.
103      * @private
104      */
105     displays_: [],
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,
114     /**
115      * The primary display.
116      * @private
117      */
118     primaryDisplay_: null,
120     /**
121      * The secondary display.
122      * @private
123      */
124     secondaryDisplay_: null,
126     /**
127      * The container div element which contains all of the display rectangles.
128      * @private
129      */
130     displaysView_: null,
132     /**
133      * The scale factor of the actual display size to the drawn display
134      * rectangle size.
135      * @private
136      */
137     visualScale_: VISUAL_SCALE,
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,
147     /**
148      * Initialize the page.
149      */
150     initializePage: function() {
151       OptionsPage.prototype.initializePage.call(this);
153       $('display-options-toggle-mirroring').onclick = function() {
154         this.mirroring_ = !this.mirroring_;
155         chrome.send('setMirroring', [this.mirroring_]);
156       }.bind(this);
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);
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);
189       chrome.send('getDisplayInfo');
190     },
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     },
203     /** @override */
204     onVisibilityChanged_: function() {
205       OptionsPage.prototype.onVisibilityChanged_(this);
206       if (this.visible)
207         chrome.send('getDisplayInfo');
208     },
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     },
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;
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       }
241       this.lastTouchLocation_ = touchLocation;
242       return this.processDragging_(e, touchLocation);
243     },
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;
254       if (e.button != 0)
255         return true;
257       e.preventDefault();
258       return this.startDragging_(e.target, {x: e.pageX, y: e.pageY});
259     },
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;
270       if (e.touches.length != 1)
271         return false;
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     },
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     },
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;
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;
322       return point;
323     },
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;
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;
345       e.preventDefault();
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       };
357       var baseDiv = this.dragging_.display.isPrimary ?
358           this.secondaryDisplay_.div : this.primaryDisplay_.div;
359       var draggingDiv = this.dragging_.display.div;
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);
366       var newCenter = {
367         x: newPosition.x + draggingDiv.offsetWidth / 2,
368         y: newPosition.y + draggingDiv.offsetHeight / 2
369       };
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       }
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       }
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       }
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       }
458       return false;
459     },
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       }
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;
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       }
497       this.updateSelectedDisplayDescription_();
498       return false;
499     },
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     },
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     },
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     },
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';
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;
601       var orientation = $('display-options-orientation-selection');
602       orientation.disabled = false;
603       var orientationOptions = orientation.getElementsByTagName('option');
604       orientationOptions[display.orientation].selected = true;
606       $('selected-display-name').textContent = display.name;
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     },
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;
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     },
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     },
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;
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_);
686       var numDisplays = Math.max(MIN_NUM_DISPLAYS, this.displays_.length);
688       var totalWidth = width + numDisplays * MIRRORING_OFFSET_PIXELS;
689       var totalHeight = height + numDisplays * MIRRORING_OFFSET_PIXELS;
691       this.displaysView_.style.height = totalHeight + 'px';
692       this.displaysView_.classList.add(
693           'display-options-displays-view-mirroring');
695       // The displays should be centered.
696       var offsetX =
697           $('display-options-displays-view').offsetWidth / 2 - totalWidth / 2;
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     },
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       }
734       // Make the margin around the bounding box.
735       var areaWidth = boundingBox.right - boundingBox.left + maxWidth;
736       var areaHeight = boundingBox.bottom - boundingBox.top + maxHeight;
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);
743       // Prepare enough area for thisplays_view by adding the maximum height.
744       this.displaysView_.style.height =
745           Math.ceil(areaHeight * this.visualScale_) + 'px';
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       };
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       };
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;
767         div.className = 'displays-display';
768         if (i == this.focusedIndex_)
769           div.classList.add('displays-focused');
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';
791         div.onmousedown = this.onMouseDown_.bind(this);
792         div.ontouchstart = this.onTouchStart_.bind(this);
794         this.displaysView_.appendChild(div);
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     },
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;
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       }
825       this.layout_ = layout;
827       $('display-options-toggle-mirroring').textContent =
828           loadTimeData.getString(
829               mirroring ? 'stopMirroring' : 'startMirroring');
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       }
840       this.mirroring_ = mirroring;
841       this.displays_ = displays;
843       this.resetDisplaysView_();
844       if (this.mirroring_)
845         this.layoutMirroringDisplays_();
846       else
847         this.layoutDisplays_();
849       this.updateSelectedDisplayDescription_();
850     }
851   };
853   DisplayOptions.setDisplayInfo = function(
854       mirroring, displays, layout, offset) {
855     DisplayOptions.getInstance().onDisplayChanged_(
856         mirroring, displays, layout, offset);
857   };
859   // Export
860   return {
861     DisplayOptions: DisplayOptions
862   };