- add sources.
[platform/framework/web/crosswalk.git] / src / chrome / browser / resources / chromeos / user_images_grid.js
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   /** @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;
11
12   /**
13    * Interval between consecutive camera presence checks in msec.
14    * @const
15    */
16   var CAMERA_CHECK_INTERVAL_MS = 3000;
17
18   /**
19    * Interval between consecutive camera liveness checks in msec.
20    * @const
21    */
22   var CAMERA_LIVENESS_CHECK_MS = 3000;
23
24   /**
25    * Number of frames recorded by takeVideo().
26    * @const
27    */
28   var RECORD_FRAMES = 48;
29
30   /**
31    * FPS at which camera stream is recorded.
32    * @const
33    */
34   var RECORD_FPS = 16;
35
36    /**
37     * Dimensions for camera capture.
38     * @const
39     */
40   var CAPTURE_SIZE = {
41     height: 480,
42     width: 480
43   };
44
45   /**
46    * Path for internal URLs.
47    * @const
48    */
49   var CHROME_THEME_PATH = 'chrome://theme';
50
51   /**
52    * Creates a new user images grid item.
53    * @param {{url: string, title: string=, decorateFn: function=,
54    *     clickHandler: function=}} imageInfo User image URL, optional title,
55    *     decorator callback and click handler.
56    * @constructor
57    * @extends {cr.ui.GridItem}
58    */
59   function UserImagesGridItem(imageInfo) {
60     var el = new GridItem(imageInfo);
61     el.__proto__ = UserImagesGridItem.prototype;
62     return el;
63   }
64
65   UserImagesGridItem.prototype = {
66     __proto__: GridItem.prototype,
67
68     /** @override */
69     decorate: function() {
70       GridItem.prototype.decorate.call(this);
71       var imageEl = cr.doc.createElement('img');
72       // Force 1x scale for chrome://theme URLs. Grid elements are much smaller
73       // than actual images so there is no need in full scale on HDPI.
74       var url = this.dataItem.url;
75       if (url.slice(0, CHROME_THEME_PATH.length) == CHROME_THEME_PATH)
76         imageEl.src = this.dataItem.url + '@1x';
77       else
78         imageEl.src = this.dataItem.url;
79       imageEl.title = this.dataItem.title || '';
80       if (typeof this.dataItem.clickHandler == 'function')
81         imageEl.addEventListener('mousedown', this.dataItem.clickHandler);
82       // Remove any garbage added by GridItem and ListItem decorators.
83       this.textContent = '';
84       this.appendChild(imageEl);
85       if (typeof this.dataItem.decorateFn == 'function')
86         this.dataItem.decorateFn(this);
87       this.setAttribute('role', 'option');
88       this.oncontextmenu = function(e) { e.preventDefault(); };
89     }
90   };
91
92   /**
93    * Creates a selection controller that wraps selection on grid ends
94    * and translates Enter presses into 'activate' events.
95    * @param {cr.ui.ListSelectionModel} selectionModel The selection model to
96    *     interact with.
97    * @param {cr.ui.Grid} grid The grid to interact with.
98    * @constructor
99    * @extends {cr.ui.GridSelectionController}
100    */
101   function UserImagesGridSelectionController(selectionModel, grid) {
102     GridSelectionController.call(this, selectionModel, grid);
103   }
104
105   UserImagesGridSelectionController.prototype = {
106     __proto__: GridSelectionController.prototype,
107
108     /** @override */
109     getIndexBefore: function(index) {
110       var result =
111           GridSelectionController.prototype.getIndexBefore.call(this, index);
112       return result == -1 ? this.getLastIndex() : result;
113     },
114
115     /** @override */
116     getIndexAfter: function(index) {
117       var result =
118           GridSelectionController.prototype.getIndexAfter.call(this, index);
119       return result == -1 ? this.getFirstIndex() : result;
120     },
121
122     /** @override */
123     handleKeyDown: function(e) {
124       if (e.keyIdentifier == 'Enter')
125         cr.dispatchSimpleEvent(this.grid_, 'activate');
126       else
127         GridSelectionController.prototype.handleKeyDown.call(this, e);
128     }
129   };
130
131   /**
132    * Creates a new user images grid element.
133    * @param {Object=} opt_propertyBag Optional properties.
134    * @constructor
135    * @extends {cr.ui.Grid}
136    */
137   var UserImagesGrid = cr.ui.define('grid');
138
139   UserImagesGrid.prototype = {
140     __proto__: Grid.prototype,
141
142     /** @override */
143     createSelectionController: function(sm) {
144       return new UserImagesGridSelectionController(sm, this);
145     },
146
147     /** @override */
148     decorate: function() {
149       Grid.prototype.decorate.call(this);
150       this.dataModel = new ArrayDataModel([]);
151       this.itemConstructor = UserImagesGridItem;
152       this.selectionModel = new ListSingleSelectionModel();
153       this.inProgramSelection_ = false;
154       this.addEventListener('dblclick', this.handleDblClick_.bind(this));
155       this.addEventListener('change', this.handleChange_.bind(this));
156       this.setAttribute('role', 'listbox');
157       this.autoExpands = true;
158     },
159
160     /**
161      * Handles double click on the image grid.
162      * @param {Event} e Double click Event.
163      * @private
164      */
165     handleDblClick_: function(e) {
166       // If a child element is double-clicked and not the grid itself, handle
167       // this as 'Enter' keypress.
168       if (e.target != this)
169         cr.dispatchSimpleEvent(this, 'activate');
170     },
171
172     /**
173      * Handles selection change.
174      * @param {Event} e Double click Event.
175      * @private
176      */
177     handleChange_: function(e) {
178       if (this.selectedItem === null)
179         return;
180
181       var oldSelectionType = this.selectionType;
182
183       // Update current selection type.
184       this.selectionType = this.selectedItem.type;
185
186       // Show grey silhouette with the same border as stock images.
187       if (/^chrome:\/\/theme\//.test(this.selectedItemUrl))
188         this.previewElement.classList.add('default-image');
189
190       this.updatePreview_();
191
192       var e = new Event('select');
193       e.oldSelectionType = oldSelectionType;
194       this.dispatchEvent(e);
195     },
196
197     /**
198      * Updates the preview image, if present.
199      * @private
200      */
201     updatePreview_: function() {
202       var url = this.selectedItemUrl;
203       if (url && this.previewImage_) {
204         if (url.slice(0, CHROME_THEME_PATH.length) == CHROME_THEME_PATH)
205           this.previewImage_.src = url + '@' + window.devicePixelRatio + 'x';
206         else
207           this.previewImage_.src = url;
208       }
209     },
210
211     /**
212      * Start camera presence check.
213      * @private
214      */
215     checkCameraPresence_: function() {
216       if (this.cameraPresentCheckTimer_) {
217         window.clearTimeout(this.cameraPresentCheckTimer_);
218         this.cameraPresentCheckTimer_ = null;
219       }
220       if (!this.cameraVideo_)
221         return;
222       chrome.send('checkCameraPresence');
223     },
224
225     /**
226      * Whether a camera is present or not.
227      * @type {boolean}
228      */
229     get cameraPresent() {
230       return this.cameraPresent_;
231     },
232     set cameraPresent(value) {
233       this.cameraPresent_ = value;
234       if (this.cameraLive)
235         this.cameraImage = null;
236       // Repeat the check after some time.
237       this.cameraPresentCheckTimer_ = window.setTimeout(
238           this.checkCameraPresence_.bind(this),
239           CAMERA_CHECK_INTERVAL_MS);
240     },
241
242     /**
243      * Whether camera is actually streaming video. May be |false| even when
244      * camera is present and shown but still initializing.
245      * @type {boolean}
246      */
247     get cameraOnline() {
248       return this.previewElement.classList.contains('online');
249     },
250     set cameraOnline(value) {
251       this.previewElement.classList[value ? 'add' : 'remove']('online');
252       if (value) {
253         this.cameraLiveCheckTimer_ = window.setInterval(
254             this.checkCameraLive_.bind(this), CAMERA_LIVENESS_CHECK_MS);
255       } else if (this.cameraLiveCheckTimer_) {
256         window.clearInterval(this.cameraLiveCheckTimer_);
257         this.cameraLiveCheckTimer_ = null;
258       }
259     },
260
261     /**
262      * Tries to starts camera stream capture.
263      * @param {function(): boolean} onAvailable Callback that is called if
264      *     camera is available. If it returns |true|, capture is started
265      *     immediately.
266      */
267     startCamera: function(onAvailable, onAbsent) {
268       this.stopCamera();
269       this.cameraStartInProgress_ = true;
270       navigator.webkitGetUserMedia(
271           {video: true},
272           this.handleCameraAvailable_.bind(this, onAvailable),
273           this.handleCameraAbsent_.bind(this));
274     },
275
276     /**
277      * Stops camera capture, if it's currently active.
278      */
279     stopCamera: function() {
280       this.cameraOnline = false;
281       if (this.cameraVideo_)
282         this.cameraVideo_.src = '';
283       if (this.cameraStream_)
284         this.cameraStream_.stop();
285       // Cancel any pending getUserMedia() checks.
286       this.cameraStartInProgress_ = false;
287     },
288
289     /**
290      * Handles successful camera check.
291      * @param {function(): boolean} onAvailable Callback to call. If it returns
292      *     |true|, capture is started immediately.
293      * @param {MediaStream} stream Stream object as returned by getUserMedia.
294      * @private
295      */
296     handleCameraAvailable_: function(onAvailable, stream) {
297       if (this.cameraStartInProgress_ && onAvailable()) {
298         this.cameraVideo_.src = window.webkitURL.createObjectURL(stream);
299         this.cameraStream_ = stream;
300       } else {
301         stream.stop();
302       }
303       this.cameraStartInProgress_ = false;
304     },
305
306     /**
307      * Handles camera check failure.
308      * @param {NavigatorUserMediaError=} err Error object.
309      * @private
310      */
311     handleCameraAbsent_: function(err) {
312       this.cameraPresent = false;
313       this.cameraOnline = false;
314       this.cameraStartInProgress_ = false;
315     },
316
317     /**
318      * Handles successful camera capture start.
319      * @private
320      */
321     handleVideoStarted_: function() {
322       this.cameraOnline = true;
323       this.handleVideoUpdate_();
324     },
325
326     /**
327      * Handles camera stream update. Called regularly (at rate no greater then
328      * 4/sec) while camera stream is live.
329      * @private
330      */
331     handleVideoUpdate_: function() {
332       this.lastFrameTime_ = new Date().getTime();
333     },
334
335     /**
336      * Checks if camera is still live by comparing the timestamp of the last
337      * 'timeupdate' event with the current time.
338      * @private
339      */
340     checkCameraLive_: function() {
341       if (new Date().getTime() - this.lastFrameTime_ >
342           CAMERA_LIVENESS_CHECK_MS) {
343         this.cameraPresent = false;
344       }
345     },
346
347     /**
348      * Type of the selected image (one of 'default', 'profile', 'camera').
349      * Setting it will update class list of |previewElement|.
350      * @type {string}
351      */
352     get selectionType() {
353       return this.selectionType_;
354     },
355     set selectionType(value) {
356       this.selectionType_ = value;
357       var previewClassList = this.previewElement.classList;
358       previewClassList[value == 'default' ? 'add' : 'remove']('default-image');
359       previewClassList[value == 'profile' ? 'add' : 'remove']('profile-image');
360       previewClassList[value == 'camera' ? 'add' : 'remove']('camera');
361
362       var setFocusIfLost = function() {
363         // Set focus to the grid, if focus is not on UI.
364         if (!document.activeElement ||
365             document.activeElement.tagName == 'BODY') {
366           $('user-image-grid').focus();
367         }
368       }
369       // Timeout guarantees processing AFTER style changes display attribute.
370       setTimeout(setFocusIfLost, 0);
371     },
372
373     /**
374      * Current image captured from camera as data URL. Setting to null will
375      * return to the live camera stream.
376      * @type {string=}
377      */
378     get cameraImage() {
379       return this.cameraImage_;
380     },
381     set cameraImage(imageUrl) {
382       this.cameraLive = !imageUrl;
383       if (this.cameraPresent && !imageUrl)
384         imageUrl = UserImagesGrid.ButtonImages.TAKE_PHOTO;
385       if (imageUrl) {
386         this.cameraImage_ = this.cameraImage_ ?
387             this.updateItem(this.cameraImage_, imageUrl, this.cameraTitle_) :
388             this.addItem(imageUrl, this.cameraTitle_, undefined, 0);
389         this.cameraImage_.type = 'camera';
390       } else {
391         this.removeItem(this.cameraImage_);
392         this.cameraImage_ = null;
393       }
394     },
395
396     /**
397      * Updates the titles for the camera element.
398      * @param {string} placeholderTitle Title when showing a placeholder.
399      * @param {string} capturedImageTitle Title when showing a captured photo.
400      */
401     setCameraTitles: function(placeholderTitle, capturedImageTitle) {
402       this.placeholderTitle_ = placeholderTitle;
403       this.capturedImageTitle_ = capturedImageTitle;
404       this.cameraTitle_ = this.placeholderTitle_;
405     },
406
407     /**
408      * True when camera is in live mode (i.e. no still photo selected).
409      * @type {boolean}
410      */
411     get cameraLive() {
412       return this.cameraLive_;
413     },
414     set cameraLive(value) {
415       this.cameraLive_ = value;
416       this.previewElement.classList[value ? 'add' : 'remove']('live');
417     },
418
419     /**
420      * Should only be queried from the 'change' event listener, true if the
421      * change event was triggered by a programmatical selection change.
422      * @type {boolean}
423      */
424     get inProgramSelection() {
425       return this.inProgramSelection_;
426     },
427
428     /**
429      * URL of the image selected.
430      * @type {string?}
431      */
432     get selectedItemUrl() {
433       var selectedItem = this.selectedItem;
434       return selectedItem ? selectedItem.url : null;
435     },
436     set selectedItemUrl(url) {
437       for (var i = 0, el; el = this.dataModel.item(i); i++) {
438         if (el.url === url)
439           this.selectedItemIndex = i;
440       }
441     },
442
443     /**
444      * Set index to the image selected.
445      * @type {number} index The index of selected image.
446      */
447     set selectedItemIndex(index) {
448       this.inProgramSelection_ = true;
449       this.selectionModel.selectedIndex = index;
450       this.inProgramSelection_ = false;
451     },
452
453     /** @override */
454     get selectedItem() {
455       var index = this.selectionModel.selectedIndex;
456       return index != -1 ? this.dataModel.item(index) : null;
457     },
458     set selectedItem(selectedItem) {
459       var index = this.indexOf(selectedItem);
460       this.inProgramSelection_ = true;
461       this.selectionModel.selectedIndex = index;
462       this.selectionModel.leadIndex = index;
463       this.inProgramSelection_ = false;
464     },
465
466     /**
467      * Element containing the preview image (the first IMG element) and the
468      * camera live stream (the first VIDEO element).
469      * @type {HTMLElement}
470      */
471     get previewElement() {
472       // TODO(ivankr): temporary hack for non-HTML5 version.
473       return this.previewElement_ || this;
474     },
475     set previewElement(value) {
476       this.previewElement_ = value;
477       this.previewImage_ = value.querySelector('img');
478       this.cameraVideo_ = value.querySelector('video');
479       this.cameraVideo_.addEventListener('canplay',
480                                          this.handleVideoStarted_.bind(this));
481       this.cameraVideo_.addEventListener('timeupdate',
482                                          this.handleVideoUpdate_.bind(this));
483       this.updatePreview_();
484       // Initialize camera state and check for its presence.
485       this.cameraLive = true;
486       this.cameraPresent = false;
487     },
488
489     /**
490      * Whether the camera live stream and photo should be flipped horizontally.
491      * If setting this property results in photo update, 'photoupdated' event
492      * will be fired with 'dataURL' property containing the photo encoded as
493      * a data URL
494      * @type {boolean}
495      */
496     get flipPhoto() {
497       return this.flipPhoto_ || false;
498     },
499     set flipPhoto(value) {
500       if (this.flipPhoto_ == value)
501         return;
502       this.flipPhoto_ = value;
503       this.previewElement.classList.toggle('flip-x');
504       if (!this.cameraLive) {
505         // Flip current still photo.
506         var e = new Event('photoupdated');
507         e.dataURL = this.flipPhoto ?
508             this.flipFrame_(this.previewImage_) : this.previewImage_.src;
509         this.dispatchEvent(e);
510       }
511     },
512
513     /**
514      * Performs photo capture from the live camera stream. 'phototaken' event
515      * will be fired as soon as captured photo is available, with 'dataURL'
516      * property containing the photo encoded as a data URL.
517      * @return {boolean} Whether photo capture was successful.
518      */
519     takePhoto: function() {
520       if (!this.cameraOnline)
521         return false;
522       var canvas = document.createElement('canvas');
523       canvas.width = CAPTURE_SIZE.width;
524       canvas.height = CAPTURE_SIZE.height;
525       this.captureFrame_(
526           this.cameraVideo_, canvas.getContext('2d'), CAPTURE_SIZE);
527       // Preload image before displaying it.
528       var previewImg = new Image();
529       previewImg.addEventListener('load', function(e) {
530         this.cameraTitle_ = this.capturedImageTitle_;
531         this.cameraImage = previewImg.src;
532       }.bind(this));
533       previewImg.src = canvas.toDataURL('image/png');
534       var e = new Event('phototaken');
535       e.dataURL = this.flipPhoto ? this.flipFrame_(canvas) : previewImg.src;
536       this.dispatchEvent(e);
537       return true;
538     },
539
540     /**
541      * Performs video capture from the live camera stream.
542      * @param {function=} opt_callback Callback that receives taken video as
543      *     data URL of a vertically stacked PNG sprite.
544      */
545     takeVideo: function(opt_callback) {
546       var canvas = document.createElement('canvas');
547       canvas.width = CAPTURE_SIZE.width;
548       canvas.height = CAPTURE_SIZE.height * RECORD_FRAMES;
549       var ctx = canvas.getContext('2d');
550       // Force canvas initialization to prevent FPS lag on the first frame.
551       ctx.fillRect(0, 0, 1, 1);
552       var captureData = {
553         callback: opt_callback,
554         canvas: canvas,
555         ctx: ctx,
556         frameNo: 0,
557         lastTimestamp: new Date().getTime()
558       };
559       captureData.timer = window.setInterval(
560           this.captureVideoFrame_.bind(this, captureData), 1000 / RECORD_FPS);
561     },
562
563     /**
564      * Discard current photo and return to the live camera stream.
565      */
566     discardPhoto: function() {
567       this.cameraTitle_ = this.placeholderTitle_;
568       this.cameraImage = null;
569     },
570
571     /**
572      * Capture a single still frame from a <video> element, placing it at the
573      * current drawing origin of a canvas context.
574      * @param {HTMLVideoElement} video Video element to capture from.
575      * @param {CanvasRenderingContext2D} ctx Canvas context to draw onto.
576      * @param {{width: number, height: number}} destSize Capture size.
577      * @private
578      */
579     captureFrame_: function(video, ctx, destSize) {
580       var width = video.videoWidth;
581       var height = video.videoHeight;
582       if (width < destSize.width || height < destSize.height) {
583         console.error('Video capture size too small: ' +
584                       width + 'x' + height + '!');
585       }
586       var src = {};
587       if (width / destSize.width > height / destSize.height) {
588         // Full height, crop left/right.
589         src.height = height;
590         src.width = height * destSize.width / destSize.height;
591       } else {
592         // Full width, crop top/bottom.
593         src.width = width;
594         src.height = width * destSize.height / destSize.width;
595       }
596       src.x = (width - src.width) / 2;
597       src.y = (height - src.height) / 2;
598       ctx.drawImage(video, src.x, src.y, src.width, src.height,
599                     0, 0, destSize.width, destSize.height);
600     },
601
602     /**
603      * Flips frame horizontally.
604      * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} source
605      *     Frame to flip.
606      * @return {string} Flipped frame as data URL.
607      */
608     flipFrame_: function(source) {
609       var canvas = document.createElement('canvas');
610       canvas.width = CAPTURE_SIZE.width;
611       canvas.height = CAPTURE_SIZE.height;
612       var ctx = canvas.getContext('2d');
613       ctx.translate(CAPTURE_SIZE.width, 0);
614       ctx.scale(-1.0, 1.0);
615       ctx.drawImage(source, 0, 0);
616       return canvas.toDataURL('image/png');
617     },
618
619     /**
620      * Capture next frame of the video being recorded after a takeVideo() call.
621      * @param {Object} data Property bag with the recorder details.
622      * @private
623      */
624     captureVideoFrame_: function(data) {
625       var lastTimestamp = new Date().getTime();
626       var delayMs = lastTimestamp - data.lastTimestamp;
627       console.error('Delay: ' + delayMs + ' (' + (1000 / delayMs + ' FPS)'));
628       data.lastTimestamp = lastTimestamp;
629
630       this.captureFrame_(this.cameraVideo_, data.ctx, CAPTURE_SIZE);
631       data.ctx.translate(0, CAPTURE_SIZE.height);
632
633       if (++data.frameNo == RECORD_FRAMES) {
634         window.clearTimeout(data.timer);
635         if (data.callback && typeof data.callback == 'function')
636           data.callback(data.canvas.toDataURL('image/png'));
637       }
638     },
639
640     /**
641      * Adds new image to the user image grid.
642      * @param {string} src Image URL.
643      * @param {string=} opt_title Image tooltip.
644      * @param {function=} opt_clickHandler Image click handler.
645      * @param {number=} opt_position If given, inserts new image into
646      *     that position (0-based) in image list.
647      * @param {function=} opt_decorateFn Function called with the list element
648      *     as argument to do any final decoration.
649      * @return {!Object} Image data inserted into the data model.
650      */
651     // TODO(ivankr): this function needs some argument list refactoring.
652     addItem: function(url, opt_title, opt_clickHandler, opt_position,
653                       opt_decorateFn) {
654       var imageInfo = {
655         url: url,
656         title: opt_title,
657         clickHandler: opt_clickHandler,
658         decorateFn: opt_decorateFn
659       };
660       this.inProgramSelection_ = true;
661       if (opt_position !== undefined)
662         this.dataModel.splice(opt_position, 0, imageInfo);
663       else
664         this.dataModel.push(imageInfo);
665       this.inProgramSelection_ = false;
666       return imageInfo;
667     },
668
669     /**
670      * Returns index of an image in grid.
671      * @param {Object} imageInfo Image data returned from addItem() call.
672      * @return {number} Image index (0-based) or -1 if image was not found.
673      */
674     indexOf: function(imageInfo) {
675       return this.dataModel.indexOf(imageInfo);
676     },
677
678     /**
679      * Replaces an image in the grid.
680      * @param {Object} imageInfo Image data returned from addItem() call.
681      * @param {string} imageUrl New image URL.
682      * @param {string=} opt_title New image tooltip (if undefined, tooltip
683      *     is left unchanged).
684      * @return {!Object} Image data of the added or updated image.
685      */
686     updateItem: function(imageInfo, imageUrl, opt_title) {
687       var imageIndex = this.indexOf(imageInfo);
688       var wasSelected = this.selectionModel.selectedIndex == imageIndex;
689       this.removeItem(imageInfo);
690       var newInfo = this.addItem(
691           imageUrl,
692           opt_title === undefined ? imageInfo.title : opt_title,
693           imageInfo.clickHandler,
694           imageIndex,
695           imageInfo.decorateFn);
696       // Update image data with the reset of the keys from the old data.
697       for (k in imageInfo) {
698         if (!(k in newInfo))
699           newInfo[k] = imageInfo[k];
700       }
701       if (wasSelected)
702         this.selectedItem = newInfo;
703       return newInfo;
704     },
705
706     /**
707      * Removes previously added image from the grid.
708      * @param {Object} imageInfo Image data returned from the addItem() call.
709      */
710     removeItem: function(imageInfo) {
711       var index = this.indexOf(imageInfo);
712       if (index != -1) {
713         var wasSelected = this.selectionModel.selectedIndex == index;
714         this.inProgramSelection_ = true;
715         this.dataModel.splice(index, 1);
716         if (wasSelected) {
717           // If item removed was selected, select the item next to it.
718           this.selectedItem = this.dataModel.item(
719               Math.min(this.dataModel.length - 1, index));
720         }
721         this.inProgramSelection_ = false;
722       }
723     },
724
725     /**
726      * Forces re-display, size re-calculation and focuses grid.
727      */
728     updateAndFocus: function() {
729       // Recalculate the measured item size.
730       this.measured_ = null;
731       this.columns = 0;
732       this.redraw();
733       this.focus();
734     }
735   };
736
737   /**
738    * URLs of special button images.
739    * @enum {string}
740    */
741   UserImagesGrid.ButtonImages = {
742     TAKE_PHOTO: 'chrome://theme/IDR_BUTTON_USER_IMAGE_TAKE_PHOTO',
743     CHOOSE_FILE: 'chrome://theme/IDR_BUTTON_USER_IMAGE_CHOOSE_FILE',
744     PROFILE_PICTURE: 'chrome://theme/IDR_PROFILE_PICTURE_LOADING'
745   };
746
747   return {
748     UserImagesGrid: UserImagesGrid
749   };
750 });