blob: 9ffc820b39ef6b6c2551b21fd8a33006ae3dcb29 [file] [log] [blame]
// Copyright (c) 2012 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
cr.exportPath('options');
/**
* Enumeration of multi display mode. These values must match the C++ values in
* ash::DisplayManager.
* @enum {number}
*/
options.MultiDisplayMode = {
EXTENDED: 0,
MIRRORING: 1,
UNIFIED: 2,
};
/**
* @typedef {{
* width: number,
* height: number,
* originalWidth: number,
* originalHeight: number,
* deviceScaleFactor: number,
* scale: number,
* refreshRate: number,
* isBest: boolean,
* selected: boolean
* }}
*/
options.DisplayMode;
/**
* @typedef {{
* profileId: number,
* name: string
* }}
*/
options.ColorProfile;
/**
* @typedef {{
* availableColorProfiles: !Array<!options.ColorProfile>,
* bounds: !options.DisplayBounds,
* colorProfileId: number,
* id: string,
* isInternal: boolean,
* isPrimary: boolean,
* layoutType: (!options.DisplayLayoutType|undefined),
* name: string,
* offset: (number|undefined),
* parentId: (string|undefined),
* resolutions: !Array<!options.DisplayMode>,
* rotation: number
* }}
*/
options.DisplayInfo;
cr.define('options', function() {
var Page = cr.ui.pageManager.Page;
var PageManager = cr.ui.pageManager.PageManager;
// The scale ratio of the display rectangle to its original size.
/** @const */ var VISUAL_SCALE = 1 / 10;
/**
* Encapsulated handling of the 'Display' page.
* @constructor
* @extends {cr.ui.pageManager.Page}
*/
function DisplayOptions() {
Page.call(this, 'display',
loadTimeData.getString('displayOptionsPageTabTitle'),
'display-options-page');
}
cr.addSingletonGetter(DisplayOptions);
DisplayOptions.prototype = {
__proto__: Page.prototype,
/**
* Whether the current output status is mirroring displays or not.
* @type {boolean}
* @private
*/
mirroring_: false,
/**
* Whether the unified desktop is enable or not.
* @type {boolean}
* @private
*/
unifiedDesktopEnabled_: false,
/**
* Whether the unified desktop option should be present.
* @type {boolean}
* @private
*/
unifiedEnabled_: false,
/**
* Whether the mirroring option should be present.
* @type {boolean}
* @private
*/
mirroredEnabled_: false,
/**
* The array of current output displays. It also contains the display
* rectangles currently rendered on screen.
* @type {!Array<!options.DisplayInfo>}
* @private
*/
displays_: [],
/**
* Manages the display layout.
* @type {?options.DisplayLayoutManager}
* @private
*/
displayLayoutManager_: null,
/**
* The id of the currently focused display, or empty for none.
* @type {string}
* @private
*/
focusedId_: '',
/**
* Drag info.
* @type {?{displayId: string,
* originalLocation: !options.DisplayPosition,
* eventLocation: !options.DisplayPosition}}
* @private
*/
dragInfo_: null,
/**
* The container div element which contains all of the display rectangles.
* @type {?Element}
* @private
*/
displaysView_: null,
/**
* The scale factor of the actual display size to the drawn display
* rectangle size.
* @type {number}
* @private
*/
visualScale_: VISUAL_SCALE,
/**
* The location where the last touch event happened. This is used to
* prevent unnecessary dragging events happen. Set to null unless it's
* during touch events.
* @type {?options.DisplayPosition}
* @private
*/
lastTouchLocation_: null,
/**
* Whether the display settings can be shown.
* @type {boolean}
* @private
*/
enabled_: true,
/** @override */
initializePage: function() {
Page.prototype.initializePage.call(this);
$('display-options-select-mirroring').onchange = (function() {
this.mirroring_ =
$('display-options-select-mirroring').value == 'mirroring';
chrome.send('setMirroring', [this.mirroring_]);
}).bind(this);
var container = $('display-options-displays-view-host');
container.onmousemove = this.onMouseMove_.bind(this);
window.addEventListener('mouseup', this.endDragging_.bind(this), true);
container.ontouchmove = this.onTouchMove_.bind(this);
container.ontouchend = this.endDragging_.bind(this);
$('display-options-set-primary').onclick = (function() {
chrome.send('setPrimary', [this.focusedId_]);
}).bind(this);
$('display-options-resolution-selection').onchange = (function(ev) {
var display = this.getDisplayInfoFromId_(this.focusedId_);
if (!display)
return;
var resolution = display.resolutions[ev.target.value];
chrome.send('setDisplayMode', [this.focusedId_, resolution]);
}).bind(this);
$('display-options-orientation-selection').onchange = (function(ev) {
var rotation = parseInt(ev.target.value, 10);
chrome.send('setRotation', [this.focusedId_, rotation]);
}).bind(this);
$('display-options-color-profile-selection').onchange = (function(ev) {
chrome.send('setColorProfile', [this.focusedId_, ev.target.value]);
}).bind(this);
$('selected-display-start-calibrating-overscan').onclick = (function() {
// Passes the target display ID. Do not specify it through URL hash,
// we do not care back/forward.
var displayOverscan = options.DisplayOverscan.getInstance();
displayOverscan.setDisplayId(this.focusedId_);
PageManager.showPageByName('displayOverscan');
chrome.send('coreOptionsUserMetricsAction',
['Options_DisplaySetOverscan']);
}).bind(this);
$('display-options-done').onclick = function() {
PageManager.closeOverlay();
};
$('display-options-toggle-unified-desktop').onclick = (function() {
this.unifiedDesktopEnabled_ = !this.unifiedDesktopEnabled_;
chrome.send('setUnifiedDesktopEnabled',
[this.unifiedDesktopEnabled_]);
}).bind(this);
},
/** @override */
didShowPage: function() {
var optionTitles = document.getElementsByClassName(
'selected-display-option-title');
var maxSize = 0;
for (var i = 0; i < optionTitles.length; i++)
maxSize = Math.max(maxSize, optionTitles[i].clientWidth);
for (var i = 0; i < optionTitles.length; i++)
optionTitles[i].style.width = maxSize + 'px';
chrome.send('getDisplayInfo');
},
/** @override */
canShowPage: function() { return this.enabled_; },
/**
* Enables or disables the page. When disabled, the page will not be able to
* open, and will close if currently opened. Also sets the enabled states of
* mirrored and unifed desktop.
* @param {boolean} uiEnabled
* @param {boolean} unifiedEnabled
* @param {boolean} mirroredEnabled
*/
setEnabled: function(uiEnabled, unifiedEnabled, mirroredEnabled) {
this.unifiedEnabled_ = unifiedEnabled;
this.mirroredEnabled_ = mirroredEnabled;
if (this.enabled_ == uiEnabled)
return;
this.enabled_ = uiEnabled;
if (!uiEnabled && this.visible)
PageManager.closeOverlay();
},
/**
* Mouse move handler for dragging display rectangle.
* @param {Event} e The mouse move event.
* @private
*/
onMouseMove_: function(e) {
return this.processDragging_(e, {x: e.pageX, y: e.pageY});
},
/**
* Touch move handler for dragging display rectangle.
* @param {Event} e The touch move event.
* @private
*/
onTouchMove_: function(e) {
if (e.touches.length != 1)
return true;
var touchLocation = {x: e.touches[0].pageX, y: e.touches[0].pageY};
// Touch move events happen even if the touch location doesn't change, but
// it doesn't need to process the dragging. Since sometimes the touch
// position changes slightly even though the user doesn't think to move
// the finger, very small move is just ignored.
/** @const */ var IGNORABLE_TOUCH_MOVE_PX = 1;
var xDiff = Math.abs(touchLocation.x - this.lastTouchLocation_.x);
var yDiff = Math.abs(touchLocation.y - this.lastTouchLocation_.y);
if (xDiff <= IGNORABLE_TOUCH_MOVE_PX &&
yDiff <= IGNORABLE_TOUCH_MOVE_PX) {
return true;
}
this.lastTouchLocation_ = touchLocation;
return this.processDragging_(e, touchLocation);
},
/**
* Mouse down handler for dragging display rectangle.
* @param {Event} e The mouse down event.
* @private
*/
onMouseDown_: function(e) {
if (this.mirroring_)
return true;
if (e.button != 0)
return true;
e.preventDefault();
var target = assertInstanceof(e.target, HTMLElement);
return this.startDragging_(target, {x: e.pageX, y: e.pageY});
},
/**
* Touch start handler for dragging display rectangle.
* @param {Event} e The touch start event.
* @private
*/
onTouchStart_: function(e) {
if (this.mirroring_)
return true;
if (e.touches.length != 1)
return false;
e.preventDefault();
var touch = e.touches[0];
this.lastTouchLocation_ = {x: touch.pageX, y: touch.pageY};
var target = assertInstanceof(e.target, HTMLElement);
return this.startDragging_(target, this.lastTouchLocation_);
},
/**
* @param {string} id
* @return {!options.DisplayInfo|undefined}
*/
getDisplayInfoFromId_(id) {
return this.displays_.find(function(display) {
return display.id == id;
});
},
/**
* Sends the display layout for the secondary display to Chrome.
* @private
*/
sendDragResult_: function() {
var layouts = [];
for (var i = 0; i < this.displays_.length; i++) {
var id = this.displays_[i].id;
layouts.push(this.displayLayoutManager_.getDisplayLayout(id));
}
chrome.send('setDisplayLayout', [layouts]);
},
/**
* Processes the actual dragging of display rectangle.
* @param {Event} e The event which triggers this drag.
* @param {options.DisplayPosition} eventLocation The location where the
* event happens.
* @private
*/
processDragging_: function(e, eventLocation) {
if (!this.dragInfo_)
return true;
e.preventDefault();
var dragInfo = this.dragInfo_;
/** @type {options.DisplayPosition} */ var newPosition = {
x: dragInfo.originalLocation.x +
(eventLocation.x - dragInfo.eventLocation.x),
y: dragInfo.originalLocation.y +
(eventLocation.y - dragInfo.eventLocation.y)
};
this.displayLayoutManager_.updatePosition(
dragInfo.displayId, newPosition);
return false;
},
/**
* Start dragging of a display rectangle.
* @param {!HTMLElement} target The event target.
* @param {!options.DisplayPosition} eventLocation The event location.
* @private
*/
startDragging_: function(target, eventLocation) {
var focused = this.displayLayoutManager_.getFocusedLayoutForDiv(target);
if (!focused)
return false;
var updateDisplayDescription = focused.id != this.focusedId_;
this.focusedId_ = focused.id;
this.displayLayoutManager_.setFocusedId(
focused.id, true /* user action */);
if (this.displayLayoutManager_.getDisplayLayoutCount() > 1) {
this.dragInfo_ = {
displayId: focused.id,
originalLocation: {
x: focused.div.offsetLeft,
y: focused.div.offsetTop
},
eventLocation: {x: eventLocation.x, y: eventLocation.y}
};
}
if (updateDisplayDescription)
this.updateSelectedDisplayDescription_();
return false;
},
/**
* finish the current dragging of displays.
* @param {Event} e The event which triggers this.
* @private
*/
endDragging_: function(e) {
this.lastTouchLocation_ = null;
if (!this.dragInfo_)
return false;
if (this.displayLayoutManager_.finalizePosition(this.dragInfo_.displayId))
this.sendDragResult_();
this.dragInfo_ = null;
return false;
},
/**
* Updates the description of selected display section for mirroring mode.
* @private
*/
updateSelectedDisplaySectionMirroring_: function() {
$('display-configuration-arrow').hidden = true;
$('display-options-set-primary').disabled = true;
$('display-options-select-mirroring').disabled = false;
$('selected-display-start-calibrating-overscan').disabled = true;
var display = this.displays_[0];
var orientation = $('display-options-orientation-selection');
orientation.disabled = false;
var orientationOptions = orientation.getElementsByTagName('option');
var orientationIndex = Math.floor(display.rotation / 90);
orientationOptions[orientationIndex].selected = true;
$('selected-display-name').textContent =
loadTimeData.getString('mirroringDisplay');
var resolution = $('display-options-resolution-selection');
var option = document.createElement('option');
option.value = 'default';
option.textContent = display.bounds.width + 'x' + display.bounds.height;
resolution.appendChild(option);
resolution.disabled = true;
},
/**
* Updates the description of selected display section when no display is
* selected.
* @private
*/
updateSelectedDisplaySectionNoSelected_: function() {
$('display-configuration-arrow').hidden = true;
$('display-options-set-primary').disabled = true;
$('display-options-select-mirroring').disabled = true;
$('selected-display-start-calibrating-overscan').disabled = true;
$('display-options-orientation-selection').disabled = true;
$('selected-display-name').textContent = '';
var resolution = $('display-options-resolution-selection');
resolution.appendChild(document.createElement('option'));
resolution.disabled = true;
},
/**
* Updates the description of selected display section for the selected
* display.
* @param {options.DisplayInfo} display The selected display object.
* @private
*/
updateSelectedDisplaySectionForDisplay_: function(display) {
var displayLayout =
this.displayLayoutManager_.getDisplayLayout(display.id);
var arrow = $('display-configuration-arrow');
arrow.hidden = false;
// Adding 1 px to the position to fit the border line and the border in
// arrow precisely.
arrow.style.top = $('display-configurations').offsetTop -
arrow.offsetHeight / 2 + 'px';
arrow.style.left = displayLayout.div.offsetLeft +
displayLayout.div.offsetWidth / 2 - arrow.offsetWidth / 2 + 'px';
$('display-options-set-primary').disabled = display.isPrimary;
$('display-options-select-mirroring').disabled = !this.mirroredEnabled_;
$('selected-display-start-calibrating-overscan').disabled =
display.isInternal;
var orientation = $('display-options-orientation-selection');
orientation.disabled = this.unifiedDesktopEnabled_;
var orientationOptions = orientation.getElementsByTagName('option');
var orientationIndex = Math.floor(display.rotation / 90);
orientationOptions[orientationIndex].selected = true;
$('selected-display-name').textContent = display.name;
var resolution = $('display-options-resolution-selection');
if (display.resolutions.length <= 1) {
var option = document.createElement('option');
option.value = 'default';
option.textContent = display.bounds.width + 'x' + display.bounds.height;
option.selected = true;
resolution.appendChild(option);
resolution.disabled = true;
} else {
var previousOption;
for (var i = 0; i < display.resolutions.length; i++) {
var option = document.createElement('option');
option.value = i;
option.textContent = display.resolutions[i].width + 'x' +
display.resolutions[i].height;
if (display.resolutions[i].isBest) {
option.textContent += ' ' +
loadTimeData.getString('annotateBest');
} else if (display.resolutions[i].isNative) {
option.textContent += ' ' +
loadTimeData.getString('annotateNative');
}
if (display.resolutions[i].deviceScaleFactor && previousOption &&
previousOption.textContent == option.textContent) {
option.textContent +=
' (' + display.resolutions[i].deviceScaleFactor + 'x)';
}
option.selected = display.resolutions[i].selected;
resolution.appendChild(option);
previousOption = option;
}
resolution.disabled = (display.resolutions.length <= 1);
}
if (display.availableColorProfiles.length <= 1) {
$('selected-display-color-profile-row').hidden = true;
} else {
$('selected-display-color-profile-row').hidden = false;
var profiles = $('display-options-color-profile-selection');
profiles.innerHTML = '';
for (var i = 0; i < display.availableColorProfiles.length; i++) {
var option = document.createElement('option');
var colorProfile = display.availableColorProfiles[i];
option.value = colorProfile.profileId;
option.textContent = colorProfile.name;
option.selected = (display.colorProfileId == colorProfile.profileId);
profiles.appendChild(option);
}
}
},
/**
* Updates the description of the selected display section.
* @private
*/
updateSelectedDisplayDescription_: function() {
var resolution = $('display-options-resolution-selection');
resolution.textContent = '';
var orientation = $('display-options-orientation-selection');
var orientationOptions = orientation.getElementsByTagName('option');
for (var i = 0; i < orientationOptions.length; i++)
orientationOptions[i].selected = false;
if (this.mirroring_) {
this.updateSelectedDisplaySectionMirroring_();
} else if (this.focusedId_ == '') {
this.updateSelectedDisplaySectionNoSelected_();
} else {
var focusedDisplay = this.getDisplayInfoFromId_(this.focusedId_);
if (focusedDisplay)
this.updateSelectedDisplaySectionForDisplay_(focusedDisplay);
}
},
/**
* Clears the drawing area for display rectangles.
* @private
*/
resetDisplaysView_: function() {
var displaysViewHost = $('display-options-displays-view-host');
displaysViewHost.removeChild(displaysViewHost.firstChild);
this.displaysView_ = document.createElement('div');
this.displaysView_.id = 'display-options-displays-view';
displaysViewHost.appendChild(this.displaysView_);
},
/**
* Lays out the display rectangles for mirroring.
* @private
*/
layoutMirroringDisplays_: function() {
// Offset pixels for secondary display rectangles. The offset includes the
// border width.
/** @const */ var MIRRORING_OFFSET_PIXELS = 3;
// Always show two displays because there must be two displays when
// the display_options is enabled. Don't rely on displays_.length because
// there is only one display from chrome's perspective in mirror mode.
/** @const */ var MIN_NUM_DISPLAYS = 2;
/** @const */ var MIRRORING_VERTICAL_MARGIN = 20;
// The width/height should be same as the first display:
var width = Math.ceil(this.displays_[0].bounds.width * this.visualScale_);
var height =
Math.ceil(this.displays_[0].bounds.height * this.visualScale_);
var numDisplays = Math.max(MIN_NUM_DISPLAYS, this.displays_.length);
var totalWidth = width + numDisplays * MIRRORING_OFFSET_PIXELS;
var totalHeight = height + numDisplays * MIRRORING_OFFSET_PIXELS;
this.displaysView_.style.height = totalHeight + 'px';
// The displays should be centered.
var offsetX =
$('display-options-displays-view').offsetWidth / 2 - totalWidth / 2;
for (var i = 0; i < numDisplays; i++) {
var div = /** @type {HTMLElement} */ (document.createElement('div'));
div.className = 'displays-display';
div.style.top = i * MIRRORING_OFFSET_PIXELS + 'px';
div.style.left = i * MIRRORING_OFFSET_PIXELS + offsetX + 'px';
div.style.width = width + 'px';
div.style.height = height + 'px';
div.style.zIndex = i;
// set 'display-mirrored' class for the background display rectangles.
if (i != numDisplays - 1)
div.classList.add('display-mirrored');
this.displaysView_.appendChild(div);
}
},
/**
* Layouts the display rectangles according to the current layout_.
* @private
*/
layoutDisplays_: function() {
// Create the layout manager. TODO(stevenjb): Elim DisplayLayoutManager()
// once DisplayLayoutManagerMulti is well tested.
if (this.displays_.length > 2)
this.displayLayoutManager_ = new options.DisplayLayoutManagerMulti();
else
this.displayLayoutManager_ = new options.DisplayLayoutManager();
// Create the display layouts.
for (var i = 0; i < this.displays_.length; i++) {
var display = this.displays_[i];
var layout = new options.DisplayLayout(
display.id, display.name, display.bounds, display.layoutType,
display.offset, display.parentId);
this.displayLayoutManager_.addDisplayLayout(layout);
}
// Calculate the display area bounds and create the divs for each display.
this.visualScale_ = this.displayLayoutManager_.createDisplayArea(
/** @type {!Element} */(this.displaysView_), VISUAL_SCALE);
this.displayLayoutManager_.setFocusedId(this.focusedId_);
this.displayLayoutManager_.setDivCallbacks(
this.onMouseDown_.bind(this), this.onTouchStart_.bind(this));
},
/**
* Called when the display arrangement has changed.
* @param {options.MultiDisplayMode} mode multi display mode.
* @param {!Array<!options.DisplayInfo>} displays The list of the display
* information.
* @private
*/
onDisplayChanged_: function(mode, displays) {
if (!this.visible)
return;
var mirroring = mode == options.MultiDisplayMode.MIRRORING;
var unifiedDesktopEnabled = mode == options.MultiDisplayMode.UNIFIED;
// Focus to the first display next to the primary one when |displays_|
// is updated.
if (mirroring) {
this.focusedId_ = '';
} else if (
this.focusedId_ == '' || this.mirroring_ != mirroring ||
this.unifiedDesktopEnabled_ != unifiedDesktopEnabled ||
this.displays_.length != displays.length) {
this.focusedId_ = displays.length > 0 ? displays[0].id : '';
}
this.displays_ = displays;
this.mirroring_ = mirroring;
this.unifiedDesktopEnabled_ = unifiedDesktopEnabled;
this.resetDisplaysView_();
if (this.mirroring_)
this.layoutMirroringDisplays_();
else
this.layoutDisplays_();
$('display-options-select-mirroring').value =
mirroring ? 'mirroring' : 'extended';
$('display-options-unified-desktop').hidden = !this.unifiedEnabled_;
$('display-options-toggle-unified-desktop').checked =
this.unifiedDesktopEnabled_;
var disableUnifiedDesktopOption =
(this.mirroring_ ||
(!this.unifiedDesktopEnabled_ && this.displays_.length == 1));
$('display-options-toggle-unified-desktop').disabled =
disableUnifiedDesktopOption;
this.updateSelectedDisplayDescription_();
}
};
DisplayOptions.setDisplayInfo = function(mode, displays) {
DisplayOptions.getInstance().onDisplayChanged_(mode, displays);
};
// Export
return {
DisplayOptions: DisplayOptions
};
});