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