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');
9 * availableColorProfiles: Array<{profileId: number, name: string}>,
10 * colorProfile: number,
13 * isInternal: boolean,
15 * resolutions: Array<{width: number, height: number, originalWidth: number,
16 * originalHeight: number, deviceScaleFactor: number, scale: number,
17 * refreshRate: number, isBest: boolean, selected: boolean}>,
19 * orientation: number,
28 * Enumeration of secondary display layout. The value has to be same as the
29 * values in ash/display/display_controller.cc.
32 options.SecondaryDisplayLayout = {
40 * Enumeration of multi display mode. The value has to be same as the
41 * values in ash/display/display_manager..
44 options.MultiDisplayMode = {
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;
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.
65 function getBoundsInPage(element) {
67 x: element.offsetLeft,
69 width: element.offsetWidth,
70 height: element.offsetHeight
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;
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
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;
99 return options.SecondaryDisplayLayout.LEFT;
101 if (point.y > bottomUpIntercept - point.x * diagonalSlope)
102 return options.SecondaryDisplayLayout.RIGHT;
104 return options.SecondaryDisplayLayout.TOP;
109 * Encapsulated handling of the 'Display' page.
111 * @extends {cr.ui.pageManager.Page}
113 function DisplayOptions() {
114 Page.call(this, 'display',
115 loadTimeData.getString('displayOptionsPageTabTitle'),
116 'display-options-page');
119 cr.addSingletonGetter(DisplayOptions);
121 DisplayOptions.prototype = {
122 __proto__: Page.prototype,
125 * Whether the current output status is mirroring displays or not.
131 * Whether the unified desktop is enable or not.
134 unifiedDesktopEnabled_: false,
137 * Whether the unified desktop option should be present.
140 showUnifiedDesktopOption_: false,
143 * The current secondary display layout.
146 layout_: options.SecondaryDisplayLayout.RIGHT,
149 * The array of current output displays. It also contains the display
150 * rectangles currently rendered on screen.
151 * @type {Array<options.DisplayInfo>}
157 * The index for the currently focused display in the options UI. null if
164 * The primary display.
167 primaryDisplay_: null,
170 * The secondary display.
173 secondaryDisplay_: null,
176 * The container div element which contains all of the display rectangles.
182 * The scale factor of the actual display size to the drawn display
186 visualScale_: VISUAL_SCALE,
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.
194 lastTouchLocation_: null,
197 * Whether the display settings can be shown.
203 initializePage: function() {
204 Page.prototype.initializePage.call(this);
206 $('display-options-select-mirroring').onchange = (function() {
208 $('display-options-select-mirroring').value == 'mirroring';
209 chrome.send('setMirroring', [this.mirroring_]);
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]);
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]);
226 $('display-options-orientation-selection').onchange = (function(ev) {
228 (this.focusedIndex_ === null) ? 0 : this.focusedIndex_;
229 chrome.send('setOrientation', [this.displays_[displayIndex].id,
232 $('display-options-color-profile-selection').onchange = (function(ev) {
233 chrome.send('setColorProfile', [this.displays_[this.focusedIndex_].id,
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']);
246 $('display-options-done').onclick = function() {
247 PageManager.closeOverlay();
250 $('display-options-toggle-unified-desktop').onclick = (function() {
251 this.unifiedDesktopEnabled_ = !this.unifiedDesktopEnabled_;
252 chrome.send('setUnifiedDesktopEnabled',
253 [this.unifiedDesktopEnabled_]);
258 didShowPage: function() {
259 var optionTitles = document.getElementsByClassName(
260 'selected-display-option-title');
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');
270 canShowPage: function() {
271 return this.enabled_;
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
281 setEnabled: function(enabled, showUnifiedDesktop) {
282 if (this.enabled_ == enabled &&
283 this.showUnifiedDesktopOption_ == showUnifiedDesktop) {
286 this.enabled_ = enabled;
287 this.showUnifiedDesktopOption_ = showUnifiedDesktop;
288 if (!enabled && this.visible)
289 PageManager.closeOverlay();
293 * Mouse move handler for dragging display rectangle.
294 * @param {Event} e The mouse move event.
297 onMouseMove_: function(e) {
298 return this.processDragging_(e, {x: e.pageX, y: e.pageY});
302 * Touch move handler for dragging display rectangle.
303 * @param {Event} e The touch move event.
306 onTouchMove_: function(e) {
307 if (e.touches.length != 1)
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) {
323 this.lastTouchLocation_ = touchLocation;
324 return this.processDragging_(e, touchLocation);
328 * Mouse down handler for dragging display rectangle.
329 * @param {Event} e The mouse down event.
332 onMouseDown_: function(e) {
340 var target = assertInstanceof(e.target, HTMLElement);
341 return this.startDragging_(target, {x: e.pageX, y: e.pageY});
345 * Touch start handler for dragging display rectangle.
346 * @param {Event} e The touch start event.
349 onTouchStart_: function(e) {
353 if (e.touches.length != 1)
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_);
364 * Collects the current data and sends it to Chrome.
367 applyResult_: function() {
368 // Offset is calculated from top or left edge.
369 var primary = this.primaryDisplay_;
370 var secondary = this.secondaryDisplay_;
372 if (this.layout_ == options.SecondaryDisplayLayout.LEFT ||
373 this.layout_ == options.SecondaryDisplayLayout.RIGHT) {
374 offset = secondary.div.offsetTop - primary.div.offsetTop;
376 offset = secondary.div.offsetLeft - primary.div.offsetLeft;
378 chrome.send('setDisplayLayout',
379 [this.layout_, offset / this.visualScale_]);
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.
393 snapToEdge_: function(point, width, basePoint, baseWidth) {
394 // If the edge of the regions is smaller than this, it will snap to the
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)
403 else if (endDiff < SNAP_DISTANCE_PX)
404 return basePoint + baseWidth - width;
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.
415 processDragging_: function(e, eventLocation) {
420 for (var i = 0; i < this.displays_.length; i++) {
421 if (this.displays_[i] == this.dragging_.display) {
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.
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)
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);
451 x: newPosition.x + draggingDiv.offsetWidth / 2,
452 y: newPosition.y + draggingDiv.offsetHeight / 2
456 x: baseDiv.offsetLeft,
457 y: baseDiv.offsetTop,
458 width: baseDiv.offsetWidth,
459 height: baseDiv.offsetHeight
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;
467 case options.SecondaryDisplayLayout.LEFT:
468 this.layout_ = this.dragging_.display.isPrimary ?
469 options.SecondaryDisplayLayout.RIGHT :
470 options.SecondaryDisplayLayout.LEFT;
472 case options.SecondaryDisplayLayout.TOP:
473 this.layout_ = this.dragging_.display.isPrimary ?
474 options.SecondaryDisplayLayout.BOTTOM :
475 options.SecondaryDisplayLayout.TOP;
477 case options.SecondaryDisplayLayout.BOTTOM:
478 this.layout_ = this.dragging_.display.isPrimary ?
479 options.SecondaryDisplayLayout.TOP :
480 options.SecondaryDisplayLayout.BOTTOM;
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 <
492 this.layout_ = this.dragging_.display.isPrimary ?
493 options.SecondaryDisplayLayout.BOTTOM :
494 options.SecondaryDisplayLayout.TOP;
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 <
502 this.layout_ = this.dragging_.display.isPrimary ?
503 options.SecondaryDisplayLayout.RIGHT :
504 options.SecondaryDisplayLayout.LEFT;
508 if (!this.dragging_.display.isPrimary) {
509 layoutToBase = this.layout_;
511 switch (this.layout_) {
512 case options.SecondaryDisplayLayout.RIGHT:
513 layoutToBase = options.SecondaryDisplayLayout.LEFT;
515 case options.SecondaryDisplayLayout.LEFT:
516 layoutToBase = options.SecondaryDisplayLayout.RIGHT;
518 case options.SecondaryDisplayLayout.TOP:
519 layoutToBase = options.SecondaryDisplayLayout.BOTTOM;
521 case options.SecondaryDisplayLayout.BOTTOM:
522 layoutToBase = options.SecondaryDisplayLayout.TOP;
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';
533 case options.SecondaryDisplayLayout.LEFT:
534 draggingDiv.style.left =
535 baseDiv.offsetLeft - draggingDiv.offsetWidth + 'px';
536 draggingDiv.style.top = newPosition.y + 'px';
538 case options.SecondaryDisplayLayout.TOP:
539 draggingDiv.style.top =
540 baseDiv.offsetTop - draggingDiv.offsetHeight + 'px';
541 draggingDiv.style.left = newPosition.x + 'px';
543 case options.SecondaryDisplayLayout.BOTTOM:
544 draggingDiv.style.top =
545 baseDiv.offsetTop + baseDiv.offsetHeight + 'px';
546 draggingDiv.style.left = newPosition.x + 'px';
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.
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;
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_)
581 display.div.classList.add('displays-focused');
582 if (this.displays_.length > 1) {
586 x: display.div.offsetLeft, y: display.div.offsetTop
588 eventLocation: eventLocation
593 if (willUpdateDisplayDescription)
594 this.updateSelectedDisplayDescription_();
599 * finish the current dragging of displays.
600 * @param {Event} e The event which triggers this.
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 +
616 baseDiv.offsetTop + baseDiv.offsetHeight -
618 draggingDiv.style.top = top + 'px';
620 var left = Math.max(draggingDiv.offsetLeft,
621 baseDiv.offsetLeft - draggingDiv.offsetWidth +
623 left = Math.min(left,
624 baseDiv.offsetLeft + baseDiv.offsetWidth -
626 draggingDiv.style.left = left + 'px';
628 var originalPosition = this.dragging_.display.originalPosition;
629 if (originalPosition.x != draggingDiv.offsetLeft ||
630 originalPosition.y != draggingDiv.offsetTop)
632 this.dragging_ = null;
638 * Updates the description of selected display section for mirroring mode.
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;
662 * Updates the description of selected display section when no display is
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;
679 * Updates the description of selected display section for the selected
681 * @param {Object} display The selected display object.
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
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 =
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;
718 for (var i = 0; i < display.resolutions.length; i++) {
719 var option = document.createElement('option');
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');
730 if (display.resolutions[i].deviceScaleFactor && previousOption &&
731 previousOption.textContent == option.textContent) {
732 option.textContent +=
733 ' (' + display.resolutions[i].deviceScaleFactor + 'x)';
735 option.selected = display.resolutions[i].selected;
736 resolution.appendChild(option);
737 previousOption = option;
739 resolution.disabled = (display.resolutions.length <= 1);
742 if (display.availableColorProfiles.length <= 1) {
743 $('selected-display-color-profile-row').hidden = true;
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;
754 display.colorProfile == colorProfile.profileId);
755 profiles.appendChild(option);
761 * Updates the description of the selected display section.
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_();
778 this.updateSelectedDisplaySectionForDisplay_(
779 this.displays_[this.focusedIndex_]);
784 * Clears the drawing area for display rectangles.
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_);
796 * Lays out the display rectangles for mirroring.
799 layoutMirroringDisplays_: function() {
800 // Offset pixels for secondary display rectangles. The offset includes the
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.
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);
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.
845 createDisplayRectangle_: function(display, focused) {
846 var div = document.createElement('div');
848 div.className = 'displays-display';
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);
871 * Layouts the display rectangles according to the current layout_.
874 layoutDisplays_: function() {
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;
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);
899 if (!this.primaryDisplay_)
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.
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)
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_) +
930 primaryDiv.style.top =
931 Math.floor(this.primaryDisplay_.y * this.visualScale_) +
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_) +
947 secondaryDiv.style.top =
948 primaryDiv.offsetTop - secondaryDiv.offsetHeight + 'px';
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_) +
957 case options.SecondaryDisplayLayout.BOTTOM:
958 secondaryDiv.style.left =
959 Math.floor(this.secondaryDisplay_.x * this.visualScale_) +
961 secondaryDiv.style.top =
962 primaryDiv.offsetTop + primaryDiv.offsetHeight + 'px';
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_) +
972 this.secondaryDisplay_.originalPosition = {
973 x: secondaryDiv.offsetLeft, y: secondaryDiv.offsetTop};
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
982 * @param {options.SecondaryDisplayLayout} layout The layout strategy.
983 * @param {number} offset The offset of the secondary display.
986 onDisplayChanged_: function(mode, displays, layout, offset) {
990 var hasExternal = false;
991 for (var i = 0; i < displays.length; i++) {
992 if (!displays[i].isInternal) {
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
1006 this.focusedIndex_ = null;
1007 } else if (this.mirroring_ != mirroring ||
1008 this.unifiedDesktopEnabled_ != unifiedDesktopEnabled ||
1009 this.displays_.length != displays.length) {
1010 this.focusedIndex_ = 0;
1013 this.mirroring_ = mirroring;
1014 this.unifiedDesktopEnabled_ = unifiedDesktopEnabled;
1015 this.displays_ = displays;
1017 this.resetDisplaysView_();
1018 if (this.mirroring_)
1019 this.layoutMirroringDisplays_();
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 =
1031 (!this.unifiedDesktopEnabled_ &&
1032 this.displays_.length == 1));
1034 $('display-options-toggle-unified-desktop').disabled =
1035 disableUnifiedDesktopOption;
1037 this.updateSelectedDisplayDescription_();
1041 DisplayOptions.setDisplayInfo = function(
1042 mode, displays, layout, offset) {
1043 DisplayOptions.getInstance().onDisplayChanged_(
1044 mode, displays, layout, offset);
1049 DisplayOptions: DisplayOptions