1// Copyright (c) 2012 The Chromium Authors. All rights reserved.
2// Use of this source code is governed by a BSD-style license that can be
3// found in the LICENSE file.
4
5cr.exportPath('options');
6
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 */
25options.DisplayInfo;
26
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 */
32options.SecondaryDisplayLayout = {
33  TOP: 0,
34  RIGHT: 1,
35  BOTTOM: 2,
36  LEFT: 3
37};
38
39cr.define('options', function() {
40  var Page = cr.ui.pageManager.Page;
41  var PageManager = cr.ui.pageManager.PageManager;
42
43  // The scale ratio of the display rectangle to its original size.
44  /** @const */ var VISUAL_SCALE = 1 / 10;
45
46  // The number of pixels to share the edges between displays.
47  /** @const */ var MIN_OFFSET_OVERLAP = 5;
48
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  }
69
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;
83
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  }
96
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  }
107
108  cr.addSingletonGetter(DisplayOptions);
109
110  DisplayOptions.prototype = {
111    __proto__: Page.prototype,
112
113    /**
114     * Whether the current output status is mirroring displays or not.
115     * @private
116     */
117    mirroring_: false,
118
119    /**
120     * The current secondary display layout.
121     * @private
122     */
123    layout_: options.SecondaryDisplayLayout.RIGHT,
124
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_: [],
132
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,
139
140    /**
141     * The primary display.
142     * @private
143     */
144    primaryDisplay_: null,
145
146    /**
147     * The secondary display.
148     * @private
149     */
150    secondaryDisplay_: null,
151
152    /**
153     * The container div element which contains all of the display rectangles.
154     * @private
155     */
156    displaysView_: null,
157
158    /**
159     * The scale factor of the actual display size to the drawn display
160     * rectangle size.
161     * @private
162     */
163    visualScale_: VISUAL_SCALE,
164
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,
172
173    /** @override */
174    initializePage: function() {
175      Page.prototype.initializePage.call(this);
176
177      $('display-options-toggle-mirroring').onclick = (function() {
178        this.mirroring_ = !this.mirroring_;
179        chrome.send('setMirroring', [this.mirroring_]);
180      }).bind(this);
181
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);
187
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    },
214
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    },
226
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    },
235
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;
244
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      }
257
258      this.lastTouchLocation_ = touchLocation;
259      return this.processDragging_(e, touchLocation);
260    },
261
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;
270
271      if (e.button != 0)
272        return true;
273
274      e.preventDefault();
275      var target = assertInstanceof(e.target, HTMLElement);
276      return this.startDragging_(target, {x: e.pageX, y: e.pageY});
277    },
278
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;
287
288      if (e.touches.length != 1)
289        return false;
290
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    },
297
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    },
316
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;
332
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;
340
341      return point;
342    },
343
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;
353
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;
363
364      e.preventDefault();
365
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      };
375
376      var baseDiv = this.dragging_.display.isPrimary ?
377          this.secondaryDisplay_.div : this.primaryDisplay_.div;
378      var draggingDiv = this.dragging_.display.div;
379
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);
384
385      var newCenter = {
386        x: newPosition.x + draggingDiv.offsetWidth / 2,
387        y: newPosition.y + draggingDiv.offsetHeight / 2
388      };
389
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      }
418
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      }
441
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      }
461
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      }
484
485      return false;
486    },
487
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      }
505
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;
511
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      }
523
524      this.updateSelectedDisplayDescription_();
525      return false;
526    },
527
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    },
567
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    },
588
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    },
605
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';
621
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;
627
628      var orientation = $('display-options-orientation-selection');
629      orientation.disabled = false;
630      var orientationOptions = orientation.getElementsByTagName('option');
631      orientationOptions[display.orientation].selected = true;
632
633      $('selected-display-name').textContent = display.name;
634
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      }
668
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    },
686
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;
698
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    },
709
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    },
721
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;
735
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_);
739
740      var numDisplays = Math.max(MIN_NUM_DISPLAYS, this.displays_.length);
741
742      var totalWidth = width + numDisplays * MIRRORING_OFFSET_PIXELS;
743      var totalHeight = height + numDisplays * MIRRORING_OFFSET_PIXELS;
744
745      this.displaysView_.style.height = totalHeight + 'px';
746      this.displaysView_.classList.add(
747          'display-options-displays-view-mirroring');
748
749      // The displays should be centered.
750      var offsetX =
751          $('display-options-displays-view').offsetWidth / 2 - totalWidth / 2;
752
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    },
767
768    /**
769     * Layouts the display rectangles according to the current layout_.
770     * @private
771     */
772    layoutDisplays_: function() {
773      var maxWidth = 0;
774      var maxHeight = 0;
775      var boundingBox = {left: 0, right: 0, top: 0, bottom: 0};
776      for (var i = 0; i < this.displays_.length; i++) {
777        var display = this.displays_[i];
778        boundingBox.left = Math.min(boundingBox.left, display.x);
779        boundingBox.right = Math.max(
780            boundingBox.right, display.x + display.width);
781        boundingBox.top = Math.min(boundingBox.top, display.y);
782        boundingBox.bottom = Math.max(
783            boundingBox.bottom, display.y + display.height);
784        maxWidth = Math.max(maxWidth, display.width);
785        maxHeight = Math.max(maxHeight, display.height);
786      }
787
788      // Make the margin around the bounding box.
789      var areaWidth = boundingBox.right - boundingBox.left + maxWidth;
790      var areaHeight = boundingBox.bottom - boundingBox.top + maxHeight;
791
792      // Calculates the scale by the width since horizontal size is more strict.
793      // TODO(mukai): Adds the check of vertical size in case.
794      this.visualScale_ = Math.min(
795          VISUAL_SCALE, this.displaysView_.offsetWidth / areaWidth);
796
797      // Prepare enough area for thisplays_view by adding the maximum height.
798      this.displaysView_.style.height =
799          Math.ceil(areaHeight * this.visualScale_) + 'px';
800
801      var boundingCenter = {
802        x: Math.floor((boundingBox.right + boundingBox.left) *
803            this.visualScale_ / 2),
804        y: Math.floor((boundingBox.bottom + boundingBox.top) *
805            this.visualScale_ / 2)
806      };
807
808      // Centering the bounding box of the display rectangles.
809      var offset = {
810        x: Math.floor(this.displaysView_.offsetWidth / 2 -
811            (boundingBox.right + boundingBox.left) * this.visualScale_ / 2),
812        y: Math.floor(this.displaysView_.offsetHeight / 2 -
813            (boundingBox.bottom + boundingBox.top) * this.visualScale_ / 2)
814      };
815
816      for (var i = 0; i < this.displays_.length; i++) {
817        var display = this.displays_[i];
818        var div = document.createElement('div');
819        display.div = div;
820
821        div.className = 'displays-display';
822        if (i == this.focusedIndex_)
823          div.classList.add('displays-focused');
824
825        if (display.isPrimary) {
826          this.primaryDisplay_ = display;
827        } else {
828          this.secondaryDisplay_ = display;
829        }
830        var displayNameContainer = document.createElement('div');
831        displayNameContainer.textContent = display.name;
832        div.appendChild(displayNameContainer);
833        display.nameContainer = displayNameContainer;
834        display.div.style.width =
835            Math.floor(display.width * this.visualScale_) + 'px';
836        var newHeight = Math.floor(display.height * this.visualScale_);
837        display.div.style.height = newHeight + 'px';
838        div.style.left =
839            Math.floor(display.x * this.visualScale_) + offset.x + 'px';
840        div.style.top =
841            Math.floor(display.y * this.visualScale_) + offset.y + 'px';
842        display.nameContainer.style.marginTop =
843            (newHeight - display.nameContainer.offsetHeight) / 2 + 'px';
844
845        div.onmousedown = this.onMouseDown_.bind(this);
846        div.ontouchstart = this.onTouchStart_.bind(this);
847
848        this.displaysView_.appendChild(div);
849
850        // Set the margin top to place the display name at the middle of the
851        // rectangle.  Note that this has to be done after it's added into the
852        // |displaysView_|.  Otherwise its offsetHeight is yet 0.
853        displayNameContainer.style.marginTop =
854            (div.offsetHeight - displayNameContainer.offsetHeight) / 2 + 'px';
855        display.originalPosition = {x: div.offsetLeft, y: div.offsetTop};
856      }
857    },
858
859    /**
860     * Called when the display arrangement has changed.
861     * @param {boolean} mirroring Whether current mode is mirroring or not.
862     * @param {Array.<options.DisplayInfo>} displays The list of the display
863     *     information.
864     * @param {options.SecondaryDisplayLayout} layout The layout strategy.
865     * @param {number} offset The offset of the secondary display.
866     * @private
867     */
868    onDisplayChanged_: function(mirroring, displays, layout, offset) {
869      if (!this.visible)
870        return;
871
872      var hasExternal = false;
873      for (var i = 0; i < displays.length; i++) {
874        if (!displays[i].isInternal) {
875          hasExternal = true;
876          break;
877        }
878      }
879
880      this.layout_ = layout;
881
882      $('display-options-toggle-mirroring').textContent =
883          loadTimeData.getString(
884              mirroring ? 'stopMirroring' : 'startMirroring');
885
886      // Focus to the first display next to the primary one when |displays| list
887      // is updated.
888      if (mirroring) {
889        this.focusedIndex_ = null;
890      } else if (this.mirroring_ != mirroring ||
891                 this.displays_.length != displays.length) {
892        this.focusedIndex_ = 0;
893      }
894
895      this.mirroring_ = mirroring;
896      this.displays_ = displays;
897
898      this.resetDisplaysView_();
899      if (this.mirroring_)
900        this.layoutMirroringDisplays_();
901      else
902        this.layoutDisplays_();
903
904      this.updateSelectedDisplayDescription_();
905    }
906  };
907
908  DisplayOptions.setDisplayInfo = function(
909      mirroring, displays, layout, offset) {
910    DisplayOptions.getInstance().onDisplayChanged_(
911        mirroring, displays, layout, offset);
912  };
913
914  // Export
915  return {
916    DisplayOptions: DisplayOptions
917  };
918});
919