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