[Metrics] Make MetricsStateManager take a callback param to check if UMA is enabled.
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / user_images_grid.js
blobb64b7aedab955ec1ab5f46b41ea1683293ee2f24
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.define('options', function() {
6 /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel;
7 /** @const */ var Grid = cr.ui.Grid;
8 /** @const */ var GridItem = cr.ui.GridItem;
9 /** @const */ var GridSelectionController = cr.ui.GridSelectionController;
10 /** @const */ var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel;
12 /**
13 * Number of frames recorded by takeVideo().
14 * @const
16 var RECORD_FRAMES = 48;
18 /**
19 * FPS at which camera stream is recorded.
20 * @const
22 var RECORD_FPS = 16;
24 /**
25 * Dimensions for camera capture.
26 * @const
28 var CAPTURE_SIZE = {
29 height: 480,
30 width: 480
33 /**
34 * Path for internal URLs.
35 * @const
37 var CHROME_THEME_PATH = 'chrome://theme';
39 /**
40 * Creates a new user images grid item.
41 * @param {{url: string, title: string=, decorateFn: function=,
42 * clickHandler: function=}} imageInfo User image URL, optional title,
43 * decorator callback and click handler.
44 * @constructor
45 * @extends {cr.ui.GridItem}
47 function UserImagesGridItem(imageInfo) {
48 var el = new GridItem(imageInfo);
49 el.__proto__ = UserImagesGridItem.prototype;
50 return el;
53 UserImagesGridItem.prototype = {
54 __proto__: GridItem.prototype,
56 /** @override */
57 decorate: function() {
58 GridItem.prototype.decorate.call(this);
59 var imageEl = cr.doc.createElement('img');
60 // Force 1x scale for chrome://theme URLs. Grid elements are much smaller
61 // than actual images so there is no need in full scale on HDPI.
62 var url = this.dataItem.url;
63 if (url.slice(0, CHROME_THEME_PATH.length) == CHROME_THEME_PATH)
64 imageEl.src = this.dataItem.url + '@1x';
65 else
66 imageEl.src = this.dataItem.url;
67 imageEl.title = this.dataItem.title || '';
68 if (typeof this.dataItem.clickHandler == 'function')
69 imageEl.addEventListener('mousedown', this.dataItem.clickHandler);
70 // Remove any garbage added by GridItem and ListItem decorators.
71 this.textContent = '';
72 this.appendChild(imageEl);
73 if (typeof this.dataItem.decorateFn == 'function')
74 this.dataItem.decorateFn(this);
75 this.setAttribute('role', 'option');
76 this.oncontextmenu = function(e) { e.preventDefault(); };
80 /**
81 * Creates a selection controller that wraps selection on grid ends
82 * and translates Enter presses into 'activate' events.
83 * @param {cr.ui.ListSelectionModel} selectionModel The selection model to
84 * interact with.
85 * @param {cr.ui.Grid} grid The grid to interact with.
86 * @constructor
87 * @extends {cr.ui.GridSelectionController}
89 function UserImagesGridSelectionController(selectionModel, grid) {
90 GridSelectionController.call(this, selectionModel, grid);
93 UserImagesGridSelectionController.prototype = {
94 __proto__: GridSelectionController.prototype,
96 /** @override */
97 getIndexBefore: function(index) {
98 var result =
99 GridSelectionController.prototype.getIndexBefore.call(this, index);
100 return result == -1 ? this.getLastIndex() : result;
103 /** @override */
104 getIndexAfter: function(index) {
105 var result =
106 GridSelectionController.prototype.getIndexAfter.call(this, index);
107 return result == -1 ? this.getFirstIndex() : result;
110 /** @override */
111 handleKeyDown: function(e) {
112 if (e.keyIdentifier == 'Enter')
113 cr.dispatchSimpleEvent(this.grid_, 'activate');
114 else
115 GridSelectionController.prototype.handleKeyDown.call(this, e);
120 * Creates a new user images grid element.
121 * @param {Object=} opt_propertyBag Optional properties.
122 * @constructor
123 * @extends {cr.ui.Grid}
125 var UserImagesGrid = cr.ui.define('grid');
127 UserImagesGrid.prototype = {
128 __proto__: Grid.prototype,
130 /** @override */
131 createSelectionController: function(sm) {
132 return new UserImagesGridSelectionController(sm, this);
135 /** @override */
136 decorate: function() {
137 Grid.prototype.decorate.call(this);
138 this.dataModel = new ArrayDataModel([]);
139 this.itemConstructor = UserImagesGridItem;
140 this.selectionModel = new ListSingleSelectionModel();
141 this.inProgramSelection_ = false;
142 this.addEventListener('dblclick', this.handleDblClick_.bind(this));
143 this.addEventListener('change', this.handleChange_.bind(this));
144 this.setAttribute('role', 'listbox');
145 this.autoExpands = true;
149 * Handles double click on the image grid.
150 * @param {Event} e Double click Event.
151 * @private
153 handleDblClick_: function(e) {
154 // If a child element is double-clicked and not the grid itself, handle
155 // this as 'Enter' keypress.
156 if (e.target != this)
157 cr.dispatchSimpleEvent(this, 'activate');
161 * Handles selection change.
162 * @param {Event} e Double click Event.
163 * @private
165 handleChange_: function(e) {
166 if (this.selectedItem === null)
167 return;
169 var oldSelectionType = this.selectionType;
171 // Update current selection type.
172 this.selectionType = this.selectedItem.type;
174 // Show grey silhouette with the same border as stock images.
175 if (/^chrome:\/\/theme\//.test(this.selectedItemUrl))
176 this.previewElement.classList.add('default-image');
178 this.updatePreview_();
180 var e = new Event('select');
181 e.oldSelectionType = oldSelectionType;
182 this.dispatchEvent(e);
186 * Updates the preview image, if present.
187 * @private
189 updatePreview_: function() {
190 var url = this.selectedItemUrl;
191 if (url && this.previewImage_) {
192 if (url.slice(0, CHROME_THEME_PATH.length) == CHROME_THEME_PATH)
193 this.previewImage_.src = url + '@' + window.devicePixelRatio + 'x';
194 else
195 this.previewImage_.src = url;
200 * Whether a camera is present or not.
201 * @type {boolean}
203 get cameraPresent() {
204 return this.cameraPresent_;
206 set cameraPresent(value) {
207 this.cameraPresent_ = value;
208 if (this.cameraLive)
209 this.cameraImage = null;
213 * Whether camera is actually streaming video. May be |false| even when
214 * camera is present and shown but still initializing.
215 * @type {boolean}
217 get cameraOnline() {
218 return this.previewElement.classList.contains('online');
220 set cameraOnline(value) {
221 this.previewElement.classList.toggle('online', value);
225 * Tries to starts camera stream capture.
226 * @param {function(): boolean} onAvailable Callback that is called if
227 * camera is available. If it returns |true|, capture is started
228 * immediately.
230 startCamera: function(onAvailable, onAbsent) {
231 this.stopCamera();
232 this.cameraStartInProgress_ = true;
233 navigator.webkitGetUserMedia(
234 {video: true},
235 this.handleCameraAvailable_.bind(this, onAvailable),
236 this.handleCameraAbsent_.bind(this));
240 * Stops camera capture, if it's currently active.
242 stopCamera: function() {
243 this.cameraOnline = false;
244 if (this.cameraVideo_)
245 this.cameraVideo_.src = '';
246 if (this.cameraStream_)
247 this.cameraStream_.stop();
248 // Cancel any pending getUserMedia() checks.
249 this.cameraStartInProgress_ = false;
253 * Handles successful camera check.
254 * @param {function(): boolean} onAvailable Callback to call. If it returns
255 * |true|, capture is started immediately.
256 * @param {MediaStream} stream Stream object as returned by getUserMedia.
257 * @private
259 handleCameraAvailable_: function(onAvailable, stream) {
260 if (this.cameraStartInProgress_ && onAvailable()) {
261 this.cameraVideo_.src = URL.createObjectURL(stream);
262 this.cameraStream_ = stream;
263 } else {
264 stream.stop();
266 this.cameraStartInProgress_ = false;
270 * Handles camera check failure.
271 * @param {NavigatorUserMediaError=} err Error object.
272 * @private
274 handleCameraAbsent_: function(err) {
275 this.cameraPresent = false;
276 this.cameraOnline = false;
277 this.cameraStartInProgress_ = false;
281 * Handles successful camera capture start.
282 * @private
284 handleVideoStarted_: function() {
285 this.cameraOnline = true;
286 this.handleVideoUpdate_();
290 * Handles camera stream update. Called regularly (at rate no greater then
291 * 4/sec) while camera stream is live.
292 * @private
294 handleVideoUpdate_: function() {
295 this.lastFrameTime_ = new Date().getTime();
299 * Type of the selected image (one of 'default', 'profile', 'camera').
300 * Setting it will update class list of |previewElement|.
301 * @type {string}
303 get selectionType() {
304 return this.selectionType_;
306 set selectionType(value) {
307 this.selectionType_ = value;
308 var previewClassList = this.previewElement.classList;
309 previewClassList[value == 'default' ? 'add' : 'remove']('default-image');
310 previewClassList[value == 'profile' ? 'add' : 'remove']('profile-image');
311 previewClassList[value == 'camera' ? 'add' : 'remove']('camera');
313 var setFocusIfLost = function() {
314 // Set focus to the grid, if focus is not on UI.
315 if (!document.activeElement ||
316 document.activeElement.tagName == 'BODY') {
317 $('user-image-grid').focus();
320 // Timeout guarantees processing AFTER style changes display attribute.
321 setTimeout(setFocusIfLost, 0);
325 * Current image captured from camera as data URL. Setting to null will
326 * return to the live camera stream.
327 * @type {string=}
329 get cameraImage() {
330 return this.cameraImage_;
332 set cameraImage(imageUrl) {
333 this.cameraLive = !imageUrl;
334 if (this.cameraPresent && !imageUrl)
335 imageUrl = UserImagesGrid.ButtonImages.TAKE_PHOTO;
336 if (imageUrl) {
337 this.cameraImage_ = this.cameraImage_ ?
338 this.updateItem(this.cameraImage_, imageUrl, this.cameraTitle_) :
339 this.addItem(imageUrl, this.cameraTitle_, undefined, 0);
340 this.cameraImage_.type = 'camera';
341 } else {
342 this.removeItem(this.cameraImage_);
343 this.cameraImage_ = null;
348 * Updates the titles for the camera element.
349 * @param {string} placeholderTitle Title when showing a placeholder.
350 * @param {string} capturedImageTitle Title when showing a captured photo.
352 setCameraTitles: function(placeholderTitle, capturedImageTitle) {
353 this.placeholderTitle_ = placeholderTitle;
354 this.capturedImageTitle_ = capturedImageTitle;
355 this.cameraTitle_ = this.placeholderTitle_;
359 * True when camera is in live mode (i.e. no still photo selected).
360 * @type {boolean}
362 get cameraLive() {
363 return this.cameraLive_;
365 set cameraLive(value) {
366 this.cameraLive_ = value;
367 this.previewElement.classList[value ? 'add' : 'remove']('live');
371 * Should only be queried from the 'change' event listener, true if the
372 * change event was triggered by a programmatical selection change.
373 * @type {boolean}
375 get inProgramSelection() {
376 return this.inProgramSelection_;
380 * URL of the image selected.
381 * @type {string?}
383 get selectedItemUrl() {
384 var selectedItem = this.selectedItem;
385 return selectedItem ? selectedItem.url : null;
387 set selectedItemUrl(url) {
388 for (var i = 0, el; el = this.dataModel.item(i); i++) {
389 if (el.url === url)
390 this.selectedItemIndex = i;
395 * Set index to the image selected.
396 * @type {number} index The index of selected image.
398 set selectedItemIndex(index) {
399 this.inProgramSelection_ = true;
400 this.selectionModel.selectedIndex = index;
401 this.inProgramSelection_ = false;
404 /** @override */
405 get selectedItem() {
406 var index = this.selectionModel.selectedIndex;
407 return index != -1 ? this.dataModel.item(index) : null;
409 set selectedItem(selectedItem) {
410 var index = this.indexOf(selectedItem);
411 this.inProgramSelection_ = true;
412 this.selectionModel.selectedIndex = index;
413 this.selectionModel.leadIndex = index;
414 this.inProgramSelection_ = false;
418 * Element containing the preview image (the first IMG element) and the
419 * camera live stream (the first VIDEO element).
420 * @type {HTMLElement}
422 get previewElement() {
423 // TODO(ivankr): temporary hack for non-HTML5 version.
424 return this.previewElement_ || this;
426 set previewElement(value) {
427 this.previewElement_ = value;
428 this.previewImage_ = value.querySelector('img');
429 this.cameraVideo_ = value.querySelector('video');
430 this.cameraVideo_.addEventListener('canplay',
431 this.handleVideoStarted_.bind(this));
432 this.cameraVideo_.addEventListener('timeupdate',
433 this.handleVideoUpdate_.bind(this));
434 this.updatePreview_();
435 // Initialize camera state and check for its presence.
436 this.cameraLive = true;
437 this.cameraPresent = false;
441 * Whether the camera live stream and photo should be flipped horizontally.
442 * If setting this property results in photo update, 'photoupdated' event
443 * will be fired with 'dataURL' property containing the photo encoded as
444 * a data URL
445 * @type {boolean}
447 get flipPhoto() {
448 return this.flipPhoto_ || false;
450 set flipPhoto(value) {
451 if (this.flipPhoto_ == value)
452 return;
453 this.flipPhoto_ = value;
454 this.previewElement.classList.toggle('flip-x', value);
455 /* TODO(merkulova): remove when webkit crbug.com/126479 is fixed. */
456 this.flipPhotoElement.classList.toggle('flip-trick', value);
457 if (!this.cameraLive) {
458 // Flip current still photo.
459 var e = new Event('photoupdated');
460 e.dataURL = this.flipPhoto ?
461 this.flipFrame_(this.previewImage_) : this.previewImage_.src;
462 this.dispatchEvent(e);
467 * Performs photo capture from the live camera stream. 'phototaken' event
468 * will be fired as soon as captured photo is available, with 'dataURL'
469 * property containing the photo encoded as a data URL.
470 * @return {boolean} Whether photo capture was successful.
472 takePhoto: function() {
473 if (!this.cameraOnline)
474 return false;
475 var canvas = document.createElement('canvas');
476 canvas.width = CAPTURE_SIZE.width;
477 canvas.height = CAPTURE_SIZE.height;
478 this.captureFrame_(
479 this.cameraVideo_, canvas.getContext('2d'), CAPTURE_SIZE);
480 // Preload image before displaying it.
481 var previewImg = new Image();
482 previewImg.addEventListener('load', function(e) {
483 this.cameraTitle_ = this.capturedImageTitle_;
484 this.cameraImage = previewImg.src;
485 }.bind(this));
486 previewImg.src = canvas.toDataURL('image/png');
487 var e = new Event('phototaken');
488 e.dataURL = this.flipPhoto ? this.flipFrame_(canvas) : previewImg.src;
489 this.dispatchEvent(e);
490 return true;
494 * Performs video capture from the live camera stream.
495 * @param {function=} opt_callback Callback that receives taken video as
496 * data URL of a vertically stacked PNG sprite.
498 takeVideo: function(opt_callback) {
499 var canvas = document.createElement('canvas');
500 canvas.width = CAPTURE_SIZE.width;
501 canvas.height = CAPTURE_SIZE.height * RECORD_FRAMES;
502 var ctx = canvas.getContext('2d');
503 // Force canvas initialization to prevent FPS lag on the first frame.
504 ctx.fillRect(0, 0, 1, 1);
505 var captureData = {
506 callback: opt_callback,
507 canvas: canvas,
508 ctx: ctx,
509 frameNo: 0,
510 lastTimestamp: new Date().getTime()
512 captureData.timer = window.setInterval(
513 this.captureVideoFrame_.bind(this, captureData), 1000 / RECORD_FPS);
517 * Discard current photo and return to the live camera stream.
519 discardPhoto: function() {
520 this.cameraTitle_ = this.placeholderTitle_;
521 this.cameraImage = null;
525 * Capture a single still frame from a <video> element, placing it at the
526 * current drawing origin of a canvas context.
527 * @param {HTMLVideoElement} video Video element to capture from.
528 * @param {CanvasRenderingContext2D} ctx Canvas context to draw onto.
529 * @param {{width: number, height: number}} destSize Capture size.
530 * @private
532 captureFrame_: function(video, ctx, destSize) {
533 var width = video.videoWidth;
534 var height = video.videoHeight;
535 if (width < destSize.width || height < destSize.height) {
536 console.error('Video capture size too small: ' +
537 width + 'x' + height + '!');
539 var src = {};
540 if (width / destSize.width > height / destSize.height) {
541 // Full height, crop left/right.
542 src.height = height;
543 src.width = height * destSize.width / destSize.height;
544 } else {
545 // Full width, crop top/bottom.
546 src.width = width;
547 src.height = width * destSize.height / destSize.width;
549 src.x = (width - src.width) / 2;
550 src.y = (height - src.height) / 2;
551 ctx.drawImage(video, src.x, src.y, src.width, src.height,
552 0, 0, destSize.width, destSize.height);
556 * Flips frame horizontally.
557 * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} source
558 * Frame to flip.
559 * @return {string} Flipped frame as data URL.
561 flipFrame_: function(source) {
562 var canvas = document.createElement('canvas');
563 canvas.width = CAPTURE_SIZE.width;
564 canvas.height = CAPTURE_SIZE.height;
565 var ctx = canvas.getContext('2d');
566 ctx.translate(CAPTURE_SIZE.width, 0);
567 ctx.scale(-1.0, 1.0);
568 ctx.drawImage(source, 0, 0);
569 return canvas.toDataURL('image/png');
573 * Capture next frame of the video being recorded after a takeVideo() call.
574 * @param {Object} data Property bag with the recorder details.
575 * @private
577 captureVideoFrame_: function(data) {
578 var lastTimestamp = new Date().getTime();
579 var delayMs = lastTimestamp - data.lastTimestamp;
580 console.error('Delay: ' + delayMs + ' (' + (1000 / delayMs + ' FPS)'));
581 data.lastTimestamp = lastTimestamp;
583 this.captureFrame_(this.cameraVideo_, data.ctx, CAPTURE_SIZE);
584 data.ctx.translate(0, CAPTURE_SIZE.height);
586 if (++data.frameNo == RECORD_FRAMES) {
587 window.clearTimeout(data.timer);
588 if (data.callback && typeof data.callback == 'function')
589 data.callback(data.canvas.toDataURL('image/png'));
594 * Adds new image to the user image grid.
595 * @param {string} src Image URL.
596 * @param {string=} opt_title Image tooltip.
597 * @param {function=} opt_clickHandler Image click handler.
598 * @param {number=} opt_position If given, inserts new image into
599 * that position (0-based) in image list.
600 * @param {function=} opt_decorateFn Function called with the list element
601 * as argument to do any final decoration.
602 * @return {!Object} Image data inserted into the data model.
604 // TODO(ivankr): this function needs some argument list refactoring.
605 addItem: function(url, opt_title, opt_clickHandler, opt_position,
606 opt_decorateFn) {
607 var imageInfo = {
608 url: url,
609 title: opt_title,
610 clickHandler: opt_clickHandler,
611 decorateFn: opt_decorateFn
613 this.inProgramSelection_ = true;
614 if (opt_position !== undefined)
615 this.dataModel.splice(opt_position, 0, imageInfo);
616 else
617 this.dataModel.push(imageInfo);
618 this.inProgramSelection_ = false;
619 return imageInfo;
623 * Returns index of an image in grid.
624 * @param {Object} imageInfo Image data returned from addItem() call.
625 * @return {number} Image index (0-based) or -1 if image was not found.
627 indexOf: function(imageInfo) {
628 return this.dataModel.indexOf(imageInfo);
632 * Replaces an image in the grid.
633 * @param {Object} imageInfo Image data returned from addItem() call.
634 * @param {string} imageUrl New image URL.
635 * @param {string=} opt_title New image tooltip (if undefined, tooltip
636 * is left unchanged).
637 * @return {!Object} Image data of the added or updated image.
639 updateItem: function(imageInfo, imageUrl, opt_title) {
640 var imageIndex = this.indexOf(imageInfo);
641 var wasSelected = this.selectionModel.selectedIndex == imageIndex;
642 this.removeItem(imageInfo);
643 var newInfo = this.addItem(
644 imageUrl,
645 opt_title === undefined ? imageInfo.title : opt_title,
646 imageInfo.clickHandler,
647 imageIndex,
648 imageInfo.decorateFn);
649 // Update image data with the reset of the keys from the old data.
650 for (k in imageInfo) {
651 if (!(k in newInfo))
652 newInfo[k] = imageInfo[k];
654 if (wasSelected)
655 this.selectedItem = newInfo;
656 return newInfo;
660 * Removes previously added image from the grid.
661 * @param {Object} imageInfo Image data returned from the addItem() call.
663 removeItem: function(imageInfo) {
664 var index = this.indexOf(imageInfo);
665 if (index != -1) {
666 var wasSelected = this.selectionModel.selectedIndex == index;
667 this.inProgramSelection_ = true;
668 this.dataModel.splice(index, 1);
669 if (wasSelected) {
670 // If item removed was selected, select the item next to it.
671 this.selectedItem = this.dataModel.item(
672 Math.min(this.dataModel.length - 1, index));
674 this.inProgramSelection_ = false;
679 * Forces re-display, size re-calculation and focuses grid.
681 updateAndFocus: function() {
682 // Recalculate the measured item size.
683 this.measured_ = null;
684 this.columns = 0;
685 this.redraw();
686 this.focus();
691 * URLs of special button images.
692 * @enum {string}
694 UserImagesGrid.ButtonImages = {
695 TAKE_PHOTO: 'chrome://theme/IDR_BUTTON_USER_IMAGE_TAKE_PHOTO',
696 CHOOSE_FILE: 'chrome://theme/IDR_BUTTON_USER_IMAGE_CHOOSE_FILE',
697 PROFILE_PICTURE: 'chrome://theme/IDR_PROFILE_PICTURE_LOADING'
700 return {
701 UserImagesGrid: UserImagesGrid