OLD | NEW |
| (Empty) |
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 | |
5 cr.define('options', function() { | |
6 var OptionsPage = options.OptionsPage; | |
7 | |
8 // The scale ratio of the display rectangle to its original size. | |
9 /** @const */ var VISUAL_SCALE = 1 / 10; | |
10 | |
11 // The number of pixels to share the edges between displays. | |
12 /** @const */ var MIN_OFFSET_OVERLAP = 5; | |
13 | |
14 /** | |
15 * Enumeration of secondary display layout. The value has to be same as the | |
16 * values in ash/monitor/monitor_controller.cc. | |
17 * @enum {number} | |
18 */ | |
19 var SecondaryDisplayLayout = { | |
20 TOP: 0, | |
21 RIGHT: 1, | |
22 BOTTOM: 2, | |
23 LEFT: 3 | |
24 }; | |
25 | |
26 /** | |
27 * Encapsulated handling of the 'Display' page. | |
28 * @constructor | |
29 */ | |
30 function DisplayOptions() { | |
31 OptionsPage.call(this, 'display', | |
32 loadTimeData.getString('displayOptionsPageTabTitle'), | |
33 'display-options'); | |
34 this.mirroring_ = false; | |
35 this.focusedIndex_ = null; | |
36 this.displays_ = []; | |
37 } | |
38 | |
39 cr.addSingletonGetter(DisplayOptions); | |
40 | |
41 DisplayOptions.prototype = { | |
42 __proto__: OptionsPage.prototype, | |
43 | |
44 /** | |
45 * Initialize the page. | |
46 */ | |
47 initializePage: function() { | |
48 OptionsPage.prototype.initializePage.call(this); | |
49 | |
50 $('display-options-toggle-mirroring').onclick = (function() { | |
51 this.mirroring_ = !this.mirroring_; | |
52 chrome.send('setMirroring', [this.mirroring_]); | |
53 }).bind(this); | |
54 | |
55 $('display-options-apply').onclick = this.applyResult_.bind(this); | |
56 chrome.send('getDisplayInfo'); | |
57 }, | |
58 | |
59 /** @override */ | |
60 onVisibilityChanged_: function() { | |
61 OptionsPage.prototype.onVisibilityChanged_(this); | |
62 if (this.visible) | |
63 chrome.send('getDisplayInfo'); | |
64 }, | |
65 | |
66 /** | |
67 * Collects the current data and sends it to Chrome. | |
68 * @private | |
69 */ | |
70 applyResult_: function() { | |
71 // Offset is calculated from top or left edge. | |
72 var primary = this.displays_[0]; | |
73 var secondary = this.displays_[1]; | |
74 var offset; | |
75 if (this.layout_ == SecondaryDisplayLayout.LEFT || | |
76 this.layout_ == SecondaryDisplayLayout.RIGHT) { | |
77 offset = secondary.div.offsetTop - primary.div.offsetTop; | |
78 } else { | |
79 offset = secondary.div.offsetLeft - primary.div.offsetLeft; | |
80 } | |
81 chrome.send('setDisplayLayout', [this.layout_, offset / VISUAL_SCALE]); | |
82 }, | |
83 | |
84 /** | |
85 * Mouse move handler for dragging display rectangle. | |
86 * @private | |
87 * @param {Event} e The mouse move event. | |
88 */ | |
89 onMouseMove_: function(e) { | |
90 if (!this.dragging_) | |
91 return true; | |
92 | |
93 var index = -1; | |
94 for (var i = 0; i < this.displays_.length; i++) { | |
95 if (this.displays_[i] == this.dragging_.display) { | |
96 index = i; | |
97 break; | |
98 } | |
99 } | |
100 if (index < 0) | |
101 return true; | |
102 | |
103 // Note that current code of moving display-rectangles doesn't work | |
104 // if there are >=3 displays. This is our assumption for M21. | |
105 // TODO(mukai): Fix the code to allow >=3 displays. | |
106 var mousePosition = { | |
107 x: e.pageX - this.dragging_.offset.x, | |
108 y: e.pageY - this.dragging_.offset.y | |
109 }; | |
110 var newPosition = { | |
111 x: mousePosition.x - this.dragging_.clickLocation.x, | |
112 y: mousePosition.y - this.dragging_.clickLocation.y | |
113 }; | |
114 | |
115 var primaryDiv = this.displays_[0].div; | |
116 var display = this.dragging_.display; | |
117 | |
118 // Separate the area into four (LEFT/RIGHT/TOP/BOTTOM) by the diagonals of | |
119 // the primary display, and decide which area the display should reside. | |
120 var diagonalSlope = primaryDiv.offsetHeight / primaryDiv.offsetWidth; | |
121 var topDownIntercept = | |
122 primaryDiv.offsetTop - primaryDiv.offsetLeft * diagonalSlope; | |
123 var bottomUpIntercept = primaryDiv.offsetTop + | |
124 primaryDiv.offsetHeight + primaryDiv.offsetLeft * diagonalSlope; | |
125 | |
126 if (mousePosition.y > | |
127 topDownIntercept + mousePosition.x * diagonalSlope) { | |
128 if (mousePosition.y > | |
129 bottomUpIntercept - mousePosition.x * diagonalSlope) | |
130 this.layout_ = SecondaryDisplayLayout.BOTTOM; | |
131 else | |
132 this.layout_ = SecondaryDisplayLayout.LEFT; | |
133 } else { | |
134 if (mousePosition.y > | |
135 bottomUpIntercept - mousePosition.x * diagonalSlope) | |
136 this.layout_ = SecondaryDisplayLayout.RIGHT; | |
137 else | |
138 this.layout_ = SecondaryDisplayLayout.TOP; | |
139 } | |
140 | |
141 if (this.layout_ == SecondaryDisplayLayout.LEFT || | |
142 this.layout_ == SecondaryDisplayLayout.RIGHT) { | |
143 if (newPosition.y > primaryDiv.offsetTop + primaryDiv.offsetHeight) | |
144 this.layout_ = SecondaryDisplayLayout.BOTTOM; | |
145 else if (newPosition.y + display.div.offsetHeight < | |
146 primaryDiv.offsetTop) | |
147 this.layout_ = SecondaryDisplayLayout.TOP; | |
148 } else { | |
149 if (newPosition.y > primaryDiv.offsetLeft + primaryDiv.offsetWidth) | |
150 this.layout_ = SecondaryDisplayLayout.RIGHT; | |
151 else if (newPosition.y + display.div.offsetWidth < | |
152 primaryDiv.offstLeft) | |
153 this.layout_ = SecondaryDisplayLayout.LEFT; | |
154 } | |
155 | |
156 switch (this.layout_) { | |
157 case SecondaryDisplayLayout.RIGHT: | |
158 display.div.style.left = | |
159 primaryDiv.offsetLeft + primaryDiv.offsetWidth + 'px'; | |
160 display.div.style.top = newPosition.y + 'px'; | |
161 break; | |
162 case SecondaryDisplayLayout.LEFT: | |
163 display.div.style.left = | |
164 primaryDiv.offsetLeft - display.div.offsetWidth + 'px'; | |
165 display.div.style.top = newPosition.y + 'px'; | |
166 break; | |
167 case SecondaryDisplayLayout.TOP: | |
168 display.div.style.top = | |
169 primaryDiv.offsetTop - display.div.offsetHeight + 'px'; | |
170 display.div.style.left = newPosition.x + 'px'; | |
171 break; | |
172 case SecondaryDisplayLayout.BOTTOM: | |
173 display.div.style.top = | |
174 primaryDiv.offsetTop + primaryDiv.offsetHeight + 'px'; | |
175 display.div.style.left = newPosition.x + 'px'; | |
176 break; | |
177 } | |
178 | |
179 return false; | |
180 }, | |
181 | |
182 /** | |
183 * Mouse down handler for dragging display rectangle. | |
184 * @private | |
185 * @param {Event} e The mouse down event. | |
186 */ | |
187 onMouseDown_: function(e) { | |
188 if (this.mirroring_) | |
189 return true; | |
190 | |
191 if (e.button != 0) | |
192 return true; | |
193 | |
194 this.focusedIndex_ = null; | |
195 for (var i = 0; i < this.displays_.length; i++) { | |
196 var display = this.displays_[i]; | |
197 if (this.displays_[i].div == e.target || | |
198 (i == 0 && $('display-launcher') == e.target)) { | |
199 this.focusedIndex_ = i; | |
200 break; | |
201 } | |
202 } | |
203 | |
204 for (var i = 0; i < this.displays_.length; i++) { | |
205 var display = this.displays_[i]; | |
206 display.div.className = 'displays-display'; | |
207 if (i != this.focusedIndex_) | |
208 continue; | |
209 | |
210 display.div.classList.add('displays-focused'); | |
211 // Do not drag the primary monitor. | |
212 if (i == 0) | |
213 continue; | |
214 | |
215 this.dragging_ = { | |
216 display: display, | |
217 clickLocation: {x: e.offsetX, y: e.offsetY}, | |
218 offset: {x: e.pageX - e.offsetX - display.div.offsetLeft, | |
219 y: e.pageY - e.offsetY - display.div.offsetTop} | |
220 }; | |
221 } | |
222 this.updateSelectedDisplayDescription_(); | |
223 return false; | |
224 }, | |
225 | |
226 /** | |
227 * Mouse up handler for dragging display rectangle. | |
228 * @private | |
229 * @param {Event} e The mouse up event. | |
230 */ | |
231 onMouseUp_: function(e) { | |
232 if (this.dragging_) { | |
233 // Make sure the dragging location is connected. | |
234 var primaryDiv = this.displays_[0].div; | |
235 var draggingDiv = this.dragging_.display.div; | |
236 if (this.layout_ == SecondaryDisplayLayout.LEFT || | |
237 this.layout_ == SecondaryDisplayLayout.RIGHT) { | |
238 var top = Math.max(draggingDiv.offsetTop, | |
239 primaryDiv.offsetTop - draggingDiv.offsetHeight + | |
240 MIN_OFFSET_OVERLAP); | |
241 top = Math.min(top, | |
242 primaryDiv.offsetTop + primaryDiv.offsetHeight - | |
243 MIN_OFFSET_OVERLAP); | |
244 draggingDiv.style.top = top + 'px'; | |
245 } else { | |
246 var left = Math.max(draggingDiv.offsetLeft, | |
247 primaryDiv.offsetLeft - draggingDiv.offsetWidth + | |
248 MIN_OFFSET_OVERLAP); | |
249 left = Math.min(left, | |
250 primaryDiv.offsetLeft + primaryDiv.offsetWidth - | |
251 MIN_OFFSET_OVERLAP); | |
252 draggingDiv.style.left = left + 'px'; | |
253 } | |
254 this.dragging_ = null; | |
255 } | |
256 this.updateSelectedDisplayDescription_(); | |
257 return false; | |
258 }, | |
259 | |
260 /** | |
261 * Updates the description of the selected display section. | |
262 * @private | |
263 */ | |
264 updateSelectedDisplayDescription_: function() { | |
265 if (this.focusedIndex_ == null || | |
266 this.displays_[this.focusedIndex_] == null) { | |
267 $('selected-display-data-container').hidden = true; | |
268 $('display-configuration-arrow').hidden = true; | |
269 return; | |
270 } | |
271 | |
272 $('selected-display-data-container').hidden = false; | |
273 var display = this.displays_[this.focusedIndex_]; | |
274 var nameElement = $('selected-display-name'); | |
275 while (nameElement.childNodes.length > 0) | |
276 nameElement.removeChild(nameElement.firstChild); | |
277 nameElement.appendChild(document.createTextNode(display.name)); | |
278 | |
279 var resolutionData = display.width + 'x' + display.height; | |
280 var resolutionElement = $('selected-display-resolution'); | |
281 while (resolutionElement.childNodes.length > 0) | |
282 resolutionElement.removeChild(resolutionElement.firstChild); | |
283 resolutionElement.appendChild(document.createTextNode(resolutionData)); | |
284 | |
285 var arrow = $('display-configuration-arrow'); | |
286 arrow.hidden = false; | |
287 arrow.style.top = | |
288 $('display-configurations').offsetTop - arrow.offsetHeight / 2 + 'px'; | |
289 arrow.style.left = display.div.offsetLeft + display.div.offsetWidth / 2 - | |
290 arrow.offsetWidth / 2 + 'px'; | |
291 }, | |
292 | |
293 /** | |
294 * Clears the drawing area for display rectangles. | |
295 * @private | |
296 */ | |
297 resetDisplaysView_: function() { | |
298 var displaysViewHost = $('display-options-displays-view-host'); | |
299 displaysViewHost.removeChild(displaysViewHost.firstChild); | |
300 this.displaysView_ = document.createElement('div'); | |
301 this.displaysView_.id = 'display-options-displays-view'; | |
302 this.displaysView_.onmousemove = this.onMouseMove_.bind(this); | |
303 this.displaysView_.onmousedown = this.onMouseDown_.bind(this); | |
304 this.displaysView_.onmouseup = this.onMouseUp_.bind(this); | |
305 displaysViewHost.appendChild(this.displaysView_); | |
306 }, | |
307 | |
308 /** | |
309 * Lays out the display rectangles for mirroring. | |
310 * @private | |
311 */ | |
312 layoutMirroringDisplays_: function() { | |
313 // Offset pixels for secondary display rectangles. | |
314 /** @const */ var MIRRORING_OFFSET_PIXELS = 2; | |
315 // Always show two displays because there must be two displays when | |
316 // the display_options is enabled. Don't rely on displays_.length because | |
317 // there is only one display from chrome's perspective in mirror mode. | |
318 /** @const */ var MIN_NUM_DISPLAYS = 2; | |
319 /** @const */ var MIRRORING_VERTICAL_MARGIN = 20; | |
320 | |
321 // The width/height should be same as the primary display: | |
322 var width = this.displays_[0].width * VISUAL_SCALE; | |
323 var height = this.displays_[0].height * VISUAL_SCALE; | |
324 | |
325 var numDisplays = Math.max(MIN_NUM_DISPLAYS, this.displays_.length); | |
326 | |
327 var totalWidth = width + numDisplays * MIRRORING_OFFSET_PIXELS; | |
328 var totalHeight = height + numDisplays * MIRRORING_OFFSET_PIXELS; | |
329 | |
330 this.displaysView_.style.height = totalHeight + 'px'; | |
331 this.displaysView_.classList.add( | |
332 'display-options-displays-view-mirroring'); | |
333 | |
334 // The displays should be centered. | |
335 var offsetX = | |
336 $('display-options-displays-view').offsetWidth / 2 - totalWidth / 2; | |
337 | |
338 for (var i = 0; i < numDisplays; i++) { | |
339 var div = document.createElement('div'); | |
340 div.className = 'displays-display'; | |
341 div.style.top = i * MIRRORING_OFFSET_PIXELS + 'px'; | |
342 div.style.left = i * MIRRORING_OFFSET_PIXELS + offsetX + 'px'; | |
343 div.style.width = width + 'px'; | |
344 div.style.height = height + 'px'; | |
345 div.style.zIndex = i; | |
346 // set 'display-mirrored' class for the background display rectangles. | |
347 if (i != numDisplays - 1) | |
348 div.classList.add('display-mirrored'); | |
349 this.displaysView_.appendChild(div); | |
350 } | |
351 }, | |
352 | |
353 /** | |
354 * Layouts the display rectangles according to the current layout_. | |
355 * @private | |
356 */ | |
357 layoutDisplays_: function() { | |
358 var totalHeight = 0; | |
359 var boundingBox = {left: 0, right: 0, top: 0, bottom: 0}; | |
360 for (var i = 0; i < this.displays_.length; i++) { | |
361 var display = this.displays_[i]; | |
362 totalHeight += display.height * VISUAL_SCALE; | |
363 boundingBox.left = Math.min(boundingBox.left, display.x * VISUAL_SCALE); | |
364 boundingBox.right = Math.max( | |
365 boundingBox.right, (display.x + display.width) * VISUAL_SCALE); | |
366 boundingBox.top = Math.min(boundingBox.top, display.y * VISUAL_SCALE); | |
367 boundingBox.bottom = Math.max( | |
368 boundingBox.bottom, (display.y + display.height) * VISUAL_SCALE); | |
369 } | |
370 | |
371 // Prepare enough area for thisplays_view by adding the maximum height. | |
372 this.displaysView_.style.height = totalHeight + 'px'; | |
373 | |
374 // Centering the bounding box of the display rectangles. | |
375 var offset = {x: $('display-options-displays-view').offsetWidth / 2 - | |
376 (boundingBox.left + boundingBox.right) / 2, | |
377 y: totalHeight / 2 - | |
378 (boundingBox.top + boundingBox.bottom) / 2}; | |
379 | |
380 | |
381 for (var i = 0; i < this.displays_.length; i++) { | |
382 var display = this.displays_[i]; | |
383 var div = document.createElement('div'); | |
384 display.div = div; | |
385 | |
386 div.className = 'displays-display'; | |
387 if (i == this.focusedIndex_) | |
388 div.classList.add('displays-focused'); | |
389 div.style.width = display.width * VISUAL_SCALE + 'px'; | |
390 div.style.height = display.height * VISUAL_SCALE + 'px'; | |
391 div.style.lineHeight = div.style.height; | |
392 if (i == 0) { | |
393 // Assumes that first display is primary and put a grey rectangle to | |
394 // denote launcher below. | |
395 var launcher = document.createElement('div'); | |
396 launcher.id = 'display-launcher'; | |
397 launcher.style.width = display.div.style.width; | |
398 div.appendChild(launcher); | |
399 } | |
400 div.style.left = display.x * VISUAL_SCALE + offset.x + 'px'; | |
401 div.style.top = display.y * VISUAL_SCALE + offset.y + 'px'; | |
402 | |
403 div.appendChild(document.createTextNode(display.name)); | |
404 | |
405 this.displaysView_.appendChild(div); | |
406 } | |
407 }, | |
408 | |
409 /** | |
410 * Called when the display arrangement has changed. | |
411 * @private | |
412 * @param {boolean} mirroring Whether current mode is mirroring or not. | |
413 * @param {Array} displays The list of the display information. | |
414 * @param {SecondaryDisplayLayout} layout The layout strategy. | |
415 * @param {number} offset The offset of the secondary display. | |
416 */ | |
417 onDisplayChanged_: function(mirroring, displays, layout, offset) { | |
418 this.mirroring_ = mirroring; | |
419 this.layout_ = layout; | |
420 this.offset_ = offset; | |
421 | |
422 $('display-options-toggle-mirroring').textContent = | |
423 loadTimeData.getString( | |
424 this.mirroring_ ? 'stopMirroring' : 'startMirroring'); | |
425 | |
426 // Focus to the first display next to the primary one when |displays| list | |
427 // is updated. | |
428 if (this.mirroring_) | |
429 this.focusedIndex_ = null; | |
430 else if (this.displays_.length != displays.length) | |
431 this.focusedIndex_ = 1; | |
432 | |
433 this.displays_ = displays; | |
434 | |
435 this.resetDisplaysView_(); | |
436 if (this.mirroring_) | |
437 this.layoutMirroringDisplays_(); | |
438 else | |
439 this.layoutDisplays_(); | |
440 this.updateSelectedDisplayDescription_(); | |
441 }, | |
442 }; | |
443 | |
444 DisplayOptions.setDisplayInfo = function( | |
445 mirroring, displays, layout, offset) { | |
446 DisplayOptions.getInstance().onDisplayChanged_( | |
447 mirroring, displays, layout, offset); | |
448 }; | |
449 | |
450 // Export | |
451 return { | |
452 DisplayOptions: DisplayOptions | |
453 }; | |
454 }); | |
OLD | NEW |