No dual_mode on Win10+ shortcuts.
[chromium-blink-merge.git] / chrome / browser / resources / options / chromeos / display_options.js
blob945416b93c757a0d45eca3d2dba25f257c7ea77b
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 * }}
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}
32 options.SecondaryDisplayLayout = {
33 TOP: 0,
34 RIGHT: 1,
35 BOTTOM: 2,
36 LEFT: 3
39 /**
40 * Enumeration of multi display mode. The value has to be same as the
41 * values in ash/display/display_manager..
42 * @enum {number}
44 options.MultiDisplayMode = {
45 EXTENDED: 0,
46 MIRRORING: 1,
47 UNIFIED: 2,
50 cr.define('options', function() {
51 var Page = cr.ui.pageManager.Page;
52 var PageManager = cr.ui.pageManager.PageManager;
54 // The scale ratio of the display rectangle to its original size.
55 /** @const */ var VISUAL_SCALE = 1 / 10;
57 // The number of pixels to share the edges between displays.
58 /** @const */ var MIN_OFFSET_OVERLAP = 5;
60 /**
61 * Calculates the bounds of |element| relative to the page.
62 * @param {HTMLElement} element The element to be known.
63 * @return {Object} The object for the bounds, with x, y, width, and height.
65 function getBoundsInPage(element) {
66 var bounds = {
67 x: element.offsetLeft,
68 y: element.offsetTop,
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;
78 return bounds;
81 /**
82 * Gets the position of |point| to |rect|, left, right, top, or bottom.
83 * @param {Object} rect The base rectangle with x, y, width, and height.
84 * @param {Object} point The point to check the position.
85 * @return {options.SecondaryDisplayLayout} The position of the calculated
86 * point.
88 function getPositionToRectangle(rect, point) {
89 // Separates the area into four (LEFT/RIGHT/TOP/BOTTOM) by the diagonals of
90 // the rect, and decides which area the display should reside.
91 var diagonalSlope = rect.height / rect.width;
92 var topDownIntercept = rect.y - rect.x * diagonalSlope;
93 var bottomUpIntercept = rect.y + rect.height + rect.x * diagonalSlope;
95 if (point.y > topDownIntercept + point.x * diagonalSlope) {
96 if (point.y > bottomUpIntercept - point.x * diagonalSlope)
97 return options.SecondaryDisplayLayout.BOTTOM;
98 else
99 return options.SecondaryDisplayLayout.LEFT;
100 } else {
101 if (point.y > bottomUpIntercept - point.x * diagonalSlope)
102 return options.SecondaryDisplayLayout.RIGHT;
103 else
104 return options.SecondaryDisplayLayout.TOP;
109 * Encapsulated handling of the 'Display' page.
110 * @constructor
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.
126 * @private
128 mirroring_: false,
131 * Whether the unified desktop is enable or not.
132 * @private
134 unifiedDesktopEnabled_: false,
137 * Whether the unified desktop option should be present.
138 * @private
140 showUnifiedDesktopOption_: false,
143 * The current secondary display layout.
144 * @private
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>}
152 * @private
154 displays_: [],
157 * The index for the currently focused display in the options UI. null if
158 * no one has focus.
159 * @private
161 focusedIndex_: null,
164 * The primary display.
165 * @private
167 primaryDisplay_: null,
170 * The secondary display.
171 * @private
173 secondaryDisplay_: null,
176 * The container div element which contains all of the display rectangles.
177 * @private
179 displaysView_: null,
182 * The scale factor of the actual display size to the drawn display
183 * rectangle size.
184 * @private
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.
192 * @private
194 lastTouchLocation_: null,
197 * Whether the display settings can be shown.
198 * @private
200 enabled_: true,
202 /** @override */
203 initializePage: function() {
204 Page.prototype.initializePage.call(this);
206 $('display-options-toggle-mirroring').onclick = (function() {
207 this.mirroring_ = !this.mirroring_;
208 chrome.send('setMirroring', [this.mirroring_]);
209 }).bind(this);
211 var container = $('display-options-displays-view-host');
212 container.onmousemove = this.onMouseMove_.bind(this);
213 window.addEventListener('mouseup', this.endDragging_.bind(this), true);
214 container.ontouchmove = this.onTouchMove_.bind(this);
215 container.ontouchend = this.endDragging_.bind(this);
217 $('display-options-set-primary').onclick = (function() {
218 chrome.send('setPrimary', [this.displays_[this.focusedIndex_].id]);
219 }).bind(this);
220 $('display-options-resolution-selection').onchange = (function(ev) {
221 var display = this.displays_[this.focusedIndex_];
222 var resolution = display.resolutions[ev.target.value];
223 chrome.send('setDisplayMode', [display.id, resolution]);
224 }).bind(this);
225 $('display-options-orientation-selection').onchange = (function(ev) {
226 var displayIndex =
227 (this.focusedIndex_ === null) ? 0 : this.focusedIndex_;
228 chrome.send('setOrientation', [this.displays_[displayIndex].id,
229 ev.target.value]);
230 }).bind(this);
231 $('display-options-color-profile-selection').onchange = (function(ev) {
232 chrome.send('setColorProfile', [this.displays_[this.focusedIndex_].id,
233 ev.target.value]);
234 }).bind(this);
235 $('selected-display-start-calibrating-overscan').onclick = (function() {
236 // Passes the target display ID. Do not specify it through URL hash,
237 // we do not care back/forward.
238 var displayOverscan = options.DisplayOverscan.getInstance();
239 displayOverscan.setDisplayId(this.displays_[this.focusedIndex_].id);
240 PageManager.showPageByName('displayOverscan');
241 chrome.send('coreOptionsUserMetricsAction',
242 ['Options_DisplaySetOverscan']);
243 }).bind(this);
245 $('display-options-done').onclick = function() {
246 PageManager.closeOverlay();
249 $('display-options-toggle-unified-desktop').onclick = (function() {
250 this.unifiedDesktopEnabled_ = !this.unifiedDesktopEnabled_;
251 chrome.send('setUnifiedDesktopEnabled',
252 [this.unifiedDesktopEnabled_]);
253 }).bind(this);
256 /** @override */
257 didShowPage: function() {
258 var optionTitles = document.getElementsByClassName(
259 'selected-display-option-title');
260 var maxSize = 0;
261 for (var i = 0; i < optionTitles.length; i++)
262 maxSize = Math.max(maxSize, optionTitles[i].clientWidth);
263 for (var i = 0; i < optionTitles.length; i++)
264 optionTitles[i].style.width = maxSize + 'px';
265 chrome.send('getDisplayInfo');
268 /** @override */
269 canShowPage: function() {
270 return this.enabled_;
274 * Enables or disables the page. When disabled, the page will not be able to
275 * open, and will close if currently opened.
276 * @param {boolean} enabled Whether the page should be enabled.
277 * @param {boolean} showUnifiedDesktop Whether the unified desktop option
278 * should be present.
280 setEnabled: function(enabled, showUnifiedDesktop) {
281 if (this.enabled_ == enabled &&
282 this.showUnifiedDesktopOption_ == showUnifiedDesktop) {
283 return;
285 this.enabled_ = enabled;
286 this.showUnifiedDesktopOption_ = showUnifiedDesktop;
287 if (!enabled && this.visible)
288 PageManager.closeOverlay();
292 * Mouse move handler for dragging display rectangle.
293 * @param {Event} e The mouse move event.
294 * @private
296 onMouseMove_: function(e) {
297 return this.processDragging_(e, {x: e.pageX, y: e.pageY});
301 * Touch move handler for dragging display rectangle.
302 * @param {Event} e The touch move event.
303 * @private
305 onTouchMove_: function(e) {
306 if (e.touches.length != 1)
307 return true;
309 var touchLocation = {x: e.touches[0].pageX, y: e.touches[0].pageY};
310 // Touch move events happen even if the touch location doesn't change, but
311 // it doesn't need to process the dragging. Since sometimes the touch
312 // position changes slightly even though the user doesn't think to move
313 // the finger, very small move is just ignored.
314 /** @const */ var IGNORABLE_TOUCH_MOVE_PX = 1;
315 var xDiff = Math.abs(touchLocation.x - this.lastTouchLocation_.x);
316 var yDiff = Math.abs(touchLocation.y - this.lastTouchLocation_.y);
317 if (xDiff <= IGNORABLE_TOUCH_MOVE_PX &&
318 yDiff <= IGNORABLE_TOUCH_MOVE_PX) {
319 return true;
322 this.lastTouchLocation_ = touchLocation;
323 return this.processDragging_(e, touchLocation);
327 * Mouse down handler for dragging display rectangle.
328 * @param {Event} e The mouse down event.
329 * @private
331 onMouseDown_: function(e) {
332 if (this.mirroring_)
333 return true;
335 if (e.button != 0)
336 return true;
338 e.preventDefault();
339 var target = assertInstanceof(e.target, HTMLElement);
340 return this.startDragging_(target, {x: e.pageX, y: e.pageY});
344 * Touch start handler for dragging display rectangle.
345 * @param {Event} e The touch start event.
346 * @private
348 onTouchStart_: function(e) {
349 if (this.mirroring_)
350 return true;
352 if (e.touches.length != 1)
353 return false;
355 e.preventDefault();
356 var touch = e.touches[0];
357 this.lastTouchLocation_ = {x: touch.pageX, y: touch.pageY};
358 var target = assertInstanceof(e.target, HTMLElement);
359 return this.startDragging_(target, this.lastTouchLocation_);
363 * Collects the current data and sends it to Chrome.
364 * @private
366 applyResult_: function() {
367 // Offset is calculated from top or left edge.
368 var primary = this.primaryDisplay_;
369 var secondary = this.secondaryDisplay_;
370 var offset;
371 if (this.layout_ == options.SecondaryDisplayLayout.LEFT ||
372 this.layout_ == options.SecondaryDisplayLayout.RIGHT) {
373 offset = secondary.div.offsetTop - primary.div.offsetTop;
374 } else {
375 offset = secondary.div.offsetLeft - primary.div.offsetLeft;
377 chrome.send('setDisplayLayout',
378 [this.layout_, offset / this.visualScale_]);
382 * Snaps the region [point, width] to [basePoint, baseWidth] if
383 * the [point, width] is close enough to the base's edge.
384 * @param {number} point The starting point of the region.
385 * @param {number} width The width of the region.
386 * @param {number} basePoint The starting point of the base region.
387 * @param {number} baseWidth The width of the base region.
388 * @return {number} The moved point. Returns point itself if it doesn't
389 * need to snap to the edge.
390 * @private
392 snapToEdge_: function(point, width, basePoint, baseWidth) {
393 // If the edge of the regions is smaller than this, it will snap to the
394 // base's edge.
395 /** @const */ var SNAP_DISTANCE_PX = 16;
397 var startDiff = Math.abs(point - basePoint);
398 var endDiff = Math.abs(point + width - (basePoint + baseWidth));
399 // Prefer the closer one if both edges are close enough.
400 if (startDiff < SNAP_DISTANCE_PX && startDiff < endDiff)
401 return basePoint;
402 else if (endDiff < SNAP_DISTANCE_PX)
403 return basePoint + baseWidth - width;
405 return point;
409 * Processes the actual dragging of display rectangle.
410 * @param {Event} e The event which triggers this drag.
411 * @param {Object} eventLocation The location where the event happens.
412 * @private
414 processDragging_: function(e, eventLocation) {
415 if (!this.dragging_)
416 return true;
418 var index = -1;
419 for (var i = 0; i < this.displays_.length; i++) {
420 if (this.displays_[i] == this.dragging_.display) {
421 index = i;
422 break;
425 if (index < 0)
426 return true;
428 e.preventDefault();
430 // Note that current code of moving display-rectangles doesn't work
431 // if there are >=3 displays. This is our assumption for M21.
432 // TODO(mukai): Fix the code to allow >=3 displays.
433 var newPosition = {
434 x: this.dragging_.originalLocation.x +
435 (eventLocation.x - this.dragging_.eventLocation.x),
436 y: this.dragging_.originalLocation.y +
437 (eventLocation.y - this.dragging_.eventLocation.y)
440 var baseDiv = this.dragging_.display.isPrimary ?
441 this.secondaryDisplay_.div : this.primaryDisplay_.div;
442 var draggingDiv = this.dragging_.display.div;
444 newPosition.x = this.snapToEdge_(newPosition.x, draggingDiv.offsetWidth,
445 baseDiv.offsetLeft, baseDiv.offsetWidth);
446 newPosition.y = this.snapToEdge_(newPosition.y, draggingDiv.offsetHeight,
447 baseDiv.offsetTop, baseDiv.offsetHeight);
449 var newCenter = {
450 x: newPosition.x + draggingDiv.offsetWidth / 2,
451 y: newPosition.y + draggingDiv.offsetHeight / 2
454 var baseBounds = {
455 x: baseDiv.offsetLeft,
456 y: baseDiv.offsetTop,
457 width: baseDiv.offsetWidth,
458 height: baseDiv.offsetHeight
460 switch (getPositionToRectangle(baseBounds, newCenter)) {
461 case options.SecondaryDisplayLayout.RIGHT:
462 this.layout_ = this.dragging_.display.isPrimary ?
463 options.SecondaryDisplayLayout.LEFT :
464 options.SecondaryDisplayLayout.RIGHT;
465 break;
466 case options.SecondaryDisplayLayout.LEFT:
467 this.layout_ = this.dragging_.display.isPrimary ?
468 options.SecondaryDisplayLayout.RIGHT :
469 options.SecondaryDisplayLayout.LEFT;
470 break;
471 case options.SecondaryDisplayLayout.TOP:
472 this.layout_ = this.dragging_.display.isPrimary ?
473 options.SecondaryDisplayLayout.BOTTOM :
474 options.SecondaryDisplayLayout.TOP;
475 break;
476 case options.SecondaryDisplayLayout.BOTTOM:
477 this.layout_ = this.dragging_.display.isPrimary ?
478 options.SecondaryDisplayLayout.TOP :
479 options.SecondaryDisplayLayout.BOTTOM;
480 break;
483 if (this.layout_ == options.SecondaryDisplayLayout.LEFT ||
484 this.layout_ == options.SecondaryDisplayLayout.RIGHT) {
485 if (newPosition.y > baseDiv.offsetTop + baseDiv.offsetHeight)
486 this.layout_ = this.dragging_.display.isPrimary ?
487 options.SecondaryDisplayLayout.TOP :
488 options.SecondaryDisplayLayout.BOTTOM;
489 else if (newPosition.y + draggingDiv.offsetHeight <
490 baseDiv.offsetTop)
491 this.layout_ = this.dragging_.display.isPrimary ?
492 options.SecondaryDisplayLayout.BOTTOM :
493 options.SecondaryDisplayLayout.TOP;
494 } else {
495 if (newPosition.x > baseDiv.offsetLeft + baseDiv.offsetWidth)
496 this.layout_ = this.dragging_.display.isPrimary ?
497 options.SecondaryDisplayLayout.LEFT :
498 options.SecondaryDisplayLayout.RIGHT;
499 else if (newPosition.x + draggingDiv.offsetWidth <
500 baseDiv.offsetLeft)
501 this.layout_ = this.dragging_.display.isPrimary ?
502 options.SecondaryDisplayLayout.RIGHT :
503 options.SecondaryDisplayLayout.LEFT;
506 var layoutToBase;
507 if (!this.dragging_.display.isPrimary) {
508 layoutToBase = this.layout_;
509 } else {
510 switch (this.layout_) {
511 case options.SecondaryDisplayLayout.RIGHT:
512 layoutToBase = options.SecondaryDisplayLayout.LEFT;
513 break;
514 case options.SecondaryDisplayLayout.LEFT:
515 layoutToBase = options.SecondaryDisplayLayout.RIGHT;
516 break;
517 case options.SecondaryDisplayLayout.TOP:
518 layoutToBase = options.SecondaryDisplayLayout.BOTTOM;
519 break;
520 case options.SecondaryDisplayLayout.BOTTOM:
521 layoutToBase = options.SecondaryDisplayLayout.TOP;
522 break;
526 switch (layoutToBase) {
527 case options.SecondaryDisplayLayout.RIGHT:
528 draggingDiv.style.left =
529 baseDiv.offsetLeft + baseDiv.offsetWidth + 'px';
530 draggingDiv.style.top = newPosition.y + 'px';
531 break;
532 case options.SecondaryDisplayLayout.LEFT:
533 draggingDiv.style.left =
534 baseDiv.offsetLeft - draggingDiv.offsetWidth + 'px';
535 draggingDiv.style.top = newPosition.y + 'px';
536 break;
537 case options.SecondaryDisplayLayout.TOP:
538 draggingDiv.style.top =
539 baseDiv.offsetTop - draggingDiv.offsetHeight + 'px';
540 draggingDiv.style.left = newPosition.x + 'px';
541 break;
542 case options.SecondaryDisplayLayout.BOTTOM:
543 draggingDiv.style.top =
544 baseDiv.offsetTop + baseDiv.offsetHeight + 'px';
545 draggingDiv.style.left = newPosition.x + 'px';
546 break;
549 return false;
553 * start dragging of a display rectangle.
554 * @param {HTMLElement} target The event target.
555 * @param {Object} eventLocation The object to hold the location where
556 * this event happens.
557 * @private
559 startDragging_: function(target, eventLocation) {
560 this.focusedIndex_ = null;
561 for (var i = 0; i < this.displays_.length; i++) {
562 var display = this.displays_[i];
563 if (display.div == target ||
564 (target.offsetParent && target.offsetParent == display.div)) {
565 this.focusedIndex_ = i;
566 break;
570 for (var i = 0; i < this.displays_.length; i++) {
571 var display = this.displays_[i];
572 display.div.className = 'displays-display';
573 if (i != this.focusedIndex_)
574 continue;
576 display.div.classList.add('displays-focused');
577 if (this.displays_.length > 1) {
578 this.dragging_ = {
579 display: display,
580 originalLocation: {
581 x: display.div.offsetLeft, y: display.div.offsetTop
583 eventLocation: eventLocation
588 this.updateSelectedDisplayDescription_();
589 return false;
593 * finish the current dragging of displays.
594 * @param {Event} e The event which triggers this.
595 * @private
597 endDragging_: function(e) {
598 this.lastTouchLocation_ = null;
599 if (this.dragging_) {
600 // Make sure the dragging location is connected.
601 var baseDiv = this.dragging_.display.isPrimary ?
602 this.secondaryDisplay_.div : this.primaryDisplay_.div;
603 var draggingDiv = this.dragging_.display.div;
604 if (this.layout_ == options.SecondaryDisplayLayout.LEFT ||
605 this.layout_ == options.SecondaryDisplayLayout.RIGHT) {
606 var top = Math.max(draggingDiv.offsetTop,
607 baseDiv.offsetTop - draggingDiv.offsetHeight +
608 MIN_OFFSET_OVERLAP);
609 top = Math.min(top,
610 baseDiv.offsetTop + baseDiv.offsetHeight -
611 MIN_OFFSET_OVERLAP);
612 draggingDiv.style.top = top + 'px';
613 } else {
614 var left = Math.max(draggingDiv.offsetLeft,
615 baseDiv.offsetLeft - draggingDiv.offsetWidth +
616 MIN_OFFSET_OVERLAP);
617 left = Math.min(left,
618 baseDiv.offsetLeft + baseDiv.offsetWidth -
619 MIN_OFFSET_OVERLAP);
620 draggingDiv.style.left = left + 'px';
622 var originalPosition = this.dragging_.display.originalPosition;
623 if (originalPosition.x != draggingDiv.offsetLeft ||
624 originalPosition.y != draggingDiv.offsetTop)
625 this.applyResult_();
626 this.dragging_ = null;
628 this.updateSelectedDisplayDescription_();
629 return false;
633 * Updates the description of selected display section for mirroring mode.
634 * @private
636 updateSelectedDisplaySectionMirroring_: function() {
637 $('display-configuration-arrow').hidden = true;
638 $('display-options-set-primary').disabled = true;
639 $('display-options-toggle-mirroring').disabled = false;
640 $('selected-display-start-calibrating-overscan').disabled = true;
641 var display = this.displays_[0];
642 var orientation = $('display-options-orientation-selection');
643 orientation.disabled = false;
644 var orientationOptions = orientation.getElementsByTagName('option');
645 orientationOptions[display.orientation].selected = true;
646 $('selected-display-name').textContent =
647 loadTimeData.getString('mirroringDisplay');
648 var resolution = $('display-options-resolution-selection');
649 var option = document.createElement('option');
650 option.value = 'default';
651 option.textContent = display.width + 'x' + display.height;
652 resolution.appendChild(option);
653 resolution.disabled = true;
657 * Updates the description of selected display section when no display is
658 * selected.
659 * @private
661 updateSelectedDisplaySectionNoSelected_: function() {
662 $('display-configuration-arrow').hidden = true;
663 $('display-options-set-primary').disabled = true;
664 $('display-options-toggle-mirroring').disabled = true;
665 $('selected-display-start-calibrating-overscan').disabled = true;
666 $('display-options-orientation-selection').disabled = true;
667 $('selected-display-name').textContent = '';
668 var resolution = $('display-options-resolution-selection');
669 resolution.appendChild(document.createElement('option'));
670 resolution.disabled = true;
674 * Updates the description of selected display section for the selected
675 * display.
676 * @param {Object} display The selected display object.
677 * @private
679 updateSelectedDisplaySectionForDisplay_: function(display) {
680 var arrow = $('display-configuration-arrow');
681 arrow.hidden = false;
682 // Adding 1 px to the position to fit the border line and the border in
683 // arrow precisely.
684 arrow.style.top = $('display-configurations').offsetTop -
685 arrow.offsetHeight / 2 + 'px';
686 arrow.style.left = display.div.offsetLeft +
687 display.div.offsetWidth / 2 - arrow.offsetWidth / 2 + 'px';
689 $('display-options-set-primary').disabled = display.isPrimary;
690 $('display-options-toggle-mirroring').disabled =
691 (this.displays_.length <= 1);
692 $('selected-display-start-calibrating-overscan').disabled =
693 display.isInternal;
695 var orientation = $('display-options-orientation-selection');
696 orientation.disabled = this.unifiedDesktopEnabled_;
698 var orientationOptions = orientation.getElementsByTagName('option');
699 orientationOptions[display.orientation].selected = true;
701 $('selected-display-name').textContent = display.name;
703 var resolution = $('display-options-resolution-selection');
704 if (display.resolutions.length <= 1) {
705 var option = document.createElement('option');
706 option.value = 'default';
707 option.textContent = display.width + 'x' + display.height;
708 option.selected = true;
709 resolution.appendChild(option);
710 resolution.disabled = true;
711 } else {
712 var previousOption;
713 for (var i = 0; i < display.resolutions.length; i++) {
714 var option = document.createElement('option');
715 option.value = i;
716 option.textContent = display.resolutions[i].width + 'x' +
717 display.resolutions[i].height;
718 if (display.resolutions[i].isBest) {
719 option.textContent += ' ' +
720 loadTimeData.getString('annotateBest');
721 } else if (display.resolutions[i].isNative) {
722 option.textContent += ' ' +
723 loadTimeData.getString('annotateNative');
725 if (display.resolutions[i].deviceScaleFactor && previousOption &&
726 previousOption.textContent == option.textContent) {
727 option.textContent +=
728 ' (' + display.resolutions[i].deviceScaleFactor + 'x)';
730 option.selected = display.resolutions[i].selected;
731 resolution.appendChild(option);
732 previousOption = option;
734 resolution.disabled = (display.resolutions.length <= 1);
737 if (display.availableColorProfiles.length <= 1) {
738 $('selected-display-color-profile-row').hidden = true;
739 } else {
740 $('selected-display-color-profile-row').hidden = false;
741 var profiles = $('display-options-color-profile-selection');
742 profiles.innerHTML = '';
743 for (var i = 0; i < display.availableColorProfiles.length; i++) {
744 var option = document.createElement('option');
745 var colorProfile = display.availableColorProfiles[i];
746 option.value = colorProfile.profileId;
747 option.textContent = colorProfile.name;
748 option.selected = (
749 display.colorProfile == colorProfile.profileId);
750 profiles.appendChild(option);
756 * Updates the description of the selected display section.
757 * @private
759 updateSelectedDisplayDescription_: function() {
760 var resolution = $('display-options-resolution-selection');
761 resolution.textContent = '';
762 var orientation = $('display-options-orientation-selection');
763 var orientationOptions = orientation.getElementsByTagName('option');
764 for (var i = 0; i < orientationOptions.length; i++)
765 orientationOptions.selected = false;
767 if (this.mirroring_) {
768 this.updateSelectedDisplaySectionMirroring_();
769 } else if (this.focusedIndex_ == null ||
770 this.displays_[this.focusedIndex_] == null) {
771 this.updateSelectedDisplaySectionNoSelected_();
772 } else {
773 this.updateSelectedDisplaySectionForDisplay_(
774 this.displays_[this.focusedIndex_]);
779 * Clears the drawing area for display rectangles.
780 * @private
782 resetDisplaysView_: function() {
783 var displaysViewHost = $('display-options-displays-view-host');
784 displaysViewHost.removeChild(displaysViewHost.firstChild);
785 this.displaysView_ = document.createElement('div');
786 this.displaysView_.id = 'display-options-displays-view';
787 displaysViewHost.appendChild(this.displaysView_);
791 * Lays out the display rectangles for mirroring.
792 * @private
794 layoutMirroringDisplays_: function() {
795 // Offset pixels for secondary display rectangles. The offset includes the
796 // border width.
797 /** @const */ var MIRRORING_OFFSET_PIXELS = 3;
798 // Always show two displays because there must be two displays when
799 // the display_options is enabled. Don't rely on displays_.length because
800 // there is only one display from chrome's perspective in mirror mode.
801 /** @const */ var MIN_NUM_DISPLAYS = 2;
802 /** @const */ var MIRRORING_VERTICAL_MARGIN = 20;
804 // The width/height should be same as the first display:
805 var width = Math.ceil(this.displays_[0].width * this.visualScale_);
806 var height = Math.ceil(this.displays_[0].height * this.visualScale_);
808 var numDisplays = Math.max(MIN_NUM_DISPLAYS, this.displays_.length);
810 var totalWidth = width + numDisplays * MIRRORING_OFFSET_PIXELS;
811 var totalHeight = height + numDisplays * MIRRORING_OFFSET_PIXELS;
813 this.displaysView_.style.height = totalHeight + 'px';
815 // The displays should be centered.
816 var offsetX =
817 $('display-options-displays-view').offsetWidth / 2 - totalWidth / 2;
819 for (var i = 0; i < numDisplays; i++) {
820 var div = document.createElement('div');
821 div.className = 'displays-display';
822 div.style.top = i * MIRRORING_OFFSET_PIXELS + 'px';
823 div.style.left = i * MIRRORING_OFFSET_PIXELS + offsetX + 'px';
824 div.style.width = width + 'px';
825 div.style.height = height + 'px';
826 div.style.zIndex = i;
827 // set 'display-mirrored' class for the background display rectangles.
828 if (i != numDisplays - 1)
829 div.classList.add('display-mirrored');
830 this.displaysView_.appendChild(div);
835 * Creates a div element representing the specified display.
836 * @param {Object} display The display object.
837 * @param {boolean} focused True if it's focused.
838 * @private
840 createDisplayRectangle_: function(display, focused) {
841 var div = document.createElement('div');
842 display.div = div;
843 div.className = 'displays-display';
844 if (focused)
845 div.classList.add('displays-focused');
847 // div needs to be added to the DOM tree first, otherwise offsetHeight for
848 // nameContainer below cannot be computed.
849 this.displaysView_.appendChild(div);
851 var nameContainer = document.createElement('div');
852 nameContainer.textContent = display.name;
853 div.appendChild(nameContainer);
854 div.style.width = Math.floor(display.width * this.visualScale_) + 'px';
855 var newHeight = Math.floor(display.height * this.visualScale_);
856 div.style.height = newHeight + 'px';
857 nameContainer.style.marginTop =
858 (newHeight - nameContainer.offsetHeight) / 2 + 'px';
860 div.onmousedown = this.onMouseDown_.bind(this);
861 div.ontouchstart = this.onTouchStart_.bind(this);
862 return div;
866 * Layouts the display rectangles according to the current layout_.
867 * @private
869 layoutDisplays_: function() {
870 var maxWidth = 0;
871 var maxHeight = 0;
872 var boundingBox = {left: 0, right: 0, top: 0, bottom: 0};
873 this.primaryDisplay_ = null;
874 this.secondaryDisplay_ = null;
875 var focusedDisplay = null;
876 for (var i = 0; i < this.displays_.length; i++) {
877 var display = this.displays_[i];
878 if (display.isPrimary)
879 this.primaryDisplay_ = display;
880 else
881 this.secondaryDisplay_ = display;
882 if (i == this.focusedIndex_)
883 focusedDisplay = display;
885 boundingBox.left = Math.min(boundingBox.left, display.x);
886 boundingBox.right = Math.max(
887 boundingBox.right, display.x + display.width);
888 boundingBox.top = Math.min(boundingBox.top, display.y);
889 boundingBox.bottom = Math.max(
890 boundingBox.bottom, display.y + display.height);
891 maxWidth = Math.max(maxWidth, display.width);
892 maxHeight = Math.max(maxHeight, display.height);
894 if (!this.primaryDisplay_)
895 return;
897 // Make the margin around the bounding box.
898 var areaWidth = boundingBox.right - boundingBox.left + maxWidth;
899 var areaHeight = boundingBox.bottom - boundingBox.top + maxHeight;
901 // Calculates the scale by the width since horizontal size is more strict.
902 // TODO(mukai): Adds the check of vertical size in case.
903 this.visualScale_ = Math.min(
904 VISUAL_SCALE, this.displaysView_.offsetWidth / areaWidth);
906 // Prepare enough area for thisplays_view by adding the maximum height.
907 this.displaysView_.style.height =
908 Math.ceil(areaHeight * this.visualScale_) + 'px';
910 // Centering the bounding box of the display rectangles.
911 var offset = {
912 x: Math.floor(this.displaysView_.offsetWidth / 2 -
913 (boundingBox.right + boundingBox.left) * this.visualScale_ / 2),
914 y: Math.floor(this.displaysView_.offsetHeight / 2 -
915 (boundingBox.bottom + boundingBox.top) * this.visualScale_ / 2)
918 // Layouting the display rectangles. First layout the primaryDisplay and
919 // then layout the secondary which is attaching to the primary.
920 var primaryDiv = this.createDisplayRectangle_(
921 this.primaryDisplay_, this.primaryDisplay_ == focusedDisplay);
922 primaryDiv.style.left =
923 Math.floor(this.primaryDisplay_.x * this.visualScale_) +
924 offset.x + 'px';
925 primaryDiv.style.top =
926 Math.floor(this.primaryDisplay_.y * this.visualScale_) +
927 offset.y + 'px';
928 this.primaryDisplay_.originalPosition = {
929 x: primaryDiv.offsetLeft, y: primaryDiv.offsetTop};
931 if (this.secondaryDisplay_) {
932 var secondaryDiv = this.createDisplayRectangle_(
933 this.secondaryDisplay_, this.secondaryDisplay_ == focusedDisplay);
934 // Don't trust the secondary display's x or y, because it may cause a
935 // 1px gap due to rounding, which will create a fake update on end
936 // dragging. See crbug.com/386401
937 switch (this.layout_) {
938 case options.SecondaryDisplayLayout.TOP:
939 secondaryDiv.style.left =
940 Math.floor(this.secondaryDisplay_.x * this.visualScale_) +
941 offset.x + 'px';
942 secondaryDiv.style.top =
943 primaryDiv.offsetTop - secondaryDiv.offsetHeight + 'px';
944 break;
945 case options.SecondaryDisplayLayout.RIGHT:
946 secondaryDiv.style.left =
947 primaryDiv.offsetLeft + primaryDiv.offsetWidth + 'px';
948 secondaryDiv.style.top =
949 Math.floor(this.secondaryDisplay_.y * this.visualScale_) +
950 offset.y + 'px';
951 break;
952 case options.SecondaryDisplayLayout.BOTTOM:
953 secondaryDiv.style.left =
954 Math.floor(this.secondaryDisplay_.x * this.visualScale_) +
955 offset.x + 'px';
956 secondaryDiv.style.top =
957 primaryDiv.offsetTop + primaryDiv.offsetHeight + 'px';
958 break;
959 case options.SecondaryDisplayLayout.LEFT:
960 secondaryDiv.style.left =
961 primaryDiv.offsetLeft - secondaryDiv.offsetWidth + 'px';
962 secondaryDiv.style.top =
963 Math.floor(this.secondaryDisplay_.y * this.visualScale_) +
964 offset.y + 'px';
965 break;
967 this.secondaryDisplay_.originalPosition = {
968 x: secondaryDiv.offsetLeft, y: secondaryDiv.offsetTop};
973 * Called when the display arrangement has changed.
974 * @param {options.MultiDisplayMode} mode multi display mode.
975 * @param {Array<options.DisplayInfo>} displays The list of the display
976 * information.
977 * @param {options.SecondaryDisplayLayout} layout The layout strategy.
978 * @param {number} offset The offset of the secondary display.
979 * @private
981 onDisplayChanged_: function(mode, displays, layout, offset) {
982 if (!this.visible)
983 return;
985 var hasExternal = false;
986 for (var i = 0; i < displays.length; i++) {
987 if (!displays[i].isInternal) {
988 hasExternal = true;
989 break;
993 this.layout_ = layout;
995 var mirroring = mode == options.MultiDisplayMode.MIRRORING;
996 var unifiedDesktopEnabled = mode == options.MultiDisplayMode.UNIFIED;
998 $('display-options-toggle-mirroring').textContent =
999 loadTimeData.getString(
1000 mirroring ? 'stopMirroring' : 'startMirroring');
1002 // Focus to the first display next to the primary one when |displays| list
1003 // is updated.
1004 if (mirroring) {
1005 this.focusedIndex_ = null;
1006 } else if (this.mirroring_ != mirroring ||
1007 this.unifiedDesktopEnabled_ != unifiedDesktopEnabled ||
1008 this.displays_.length != displays.length) {
1009 this.focusedIndex_ = 0;
1012 this.mirroring_ = mirroring;
1013 this.unifiedDesktopEnabled_ = unifiedDesktopEnabled;
1014 this.displays_ = displays;
1016 this.resetDisplaysView_();
1017 if (this.mirroring_)
1018 this.layoutMirroringDisplays_();
1019 else
1020 this.layoutDisplays_();
1022 $('display-options-unified-desktop').hidden =
1023 !this.showUnifiedDesktopOption_;
1025 $('display-options-toggle-unified-desktop').checked =
1026 this.unifiedDesktopEnabled_;
1028 var disableUnifiedDesktopOption =
1029 (this.mirroring_ ||
1030 (!this.unifiedDesktopEnabled_ &&
1031 this.displays_.length == 1));
1033 $('display-options-toggle-unified-desktop').disabled =
1034 disableUnifiedDesktopOption;
1036 this.updateSelectedDisplayDescription_();
1040 DisplayOptions.setDisplayInfo = function(
1041 mode, displays, layout, offset) {
1042 DisplayOptions.getInstance().onDisplayChanged_(
1043 mode, displays, layout, offset);
1046 // Export
1047 return {
1048 DisplayOptions: DisplayOptions