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
5cr.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', value);
504      /* TODO(merkulova): remove when webkit crbug.com/126479 is fixed. */
505      this.flipPhotoElement.classList.toggle('flip-trick', value);
506      if (!this.cameraLive) {
507        // Flip current still photo.
508        var e = new Event('photoupdated');
509        e.dataURL = this.flipPhoto ?
510            this.flipFrame_(this.previewImage_) : this.previewImage_.src;
511        this.dispatchEvent(e);
512      }
513    },
514
515    /**
516     * Performs photo capture from the live camera stream. 'phototaken' event
517     * will be fired as soon as captured photo is available, with 'dataURL'
518     * property containing the photo encoded as a data URL.
519     * @return {boolean} Whether photo capture was successful.
520     */
521    takePhoto: function() {
522      if (!this.cameraOnline)
523        return false;
524      var canvas = document.createElement('canvas');
525      canvas.width = CAPTURE_SIZE.width;
526      canvas.height = CAPTURE_SIZE.height;
527      this.captureFrame_(
528          this.cameraVideo_, canvas.getContext('2d'), CAPTURE_SIZE);
529      // Preload image before displaying it.
530      var previewImg = new Image();
531      previewImg.addEventListener('load', function(e) {
532        this.cameraTitle_ = this.capturedImageTitle_;
533        this.cameraImage = previewImg.src;
534      }.bind(this));
535      previewImg.src = canvas.toDataURL('image/png');
536      var e = new Event('phototaken');
537      e.dataURL = this.flipPhoto ? this.flipFrame_(canvas) : previewImg.src;
538      this.dispatchEvent(e);
539      return true;
540    },
541
542    /**
543     * Performs video capture from the live camera stream.
544     * @param {function=} opt_callback Callback that receives taken video as
545     *     data URL of a vertically stacked PNG sprite.
546     */
547    takeVideo: function(opt_callback) {
548      var canvas = document.createElement('canvas');
549      canvas.width = CAPTURE_SIZE.width;
550      canvas.height = CAPTURE_SIZE.height * RECORD_FRAMES;
551      var ctx = canvas.getContext('2d');
552      // Force canvas initialization to prevent FPS lag on the first frame.
553      ctx.fillRect(0, 0, 1, 1);
554      var captureData = {
555        callback: opt_callback,
556        canvas: canvas,
557        ctx: ctx,
558        frameNo: 0,
559        lastTimestamp: new Date().getTime()
560      };
561      captureData.timer = window.setInterval(
562          this.captureVideoFrame_.bind(this, captureData), 1000 / RECORD_FPS);
563    },
564
565    /**
566     * Discard current photo and return to the live camera stream.
567     */
568    discardPhoto: function() {
569      this.cameraTitle_ = this.placeholderTitle_;
570      this.cameraImage = null;
571    },
572
573    /**
574     * Capture a single still frame from a <video> element, placing it at the
575     * current drawing origin of a canvas context.
576     * @param {HTMLVideoElement} video Video element to capture from.
577     * @param {CanvasRenderingContext2D} ctx Canvas context to draw onto.
578     * @param {{width: number, height: number}} destSize Capture size.
579     * @private
580     */
581    captureFrame_: function(video, ctx, destSize) {
582      var width = video.videoWidth;
583      var height = video.videoHeight;
584      if (width < destSize.width || height < destSize.height) {
585        console.error('Video capture size too small: ' +
586                      width + 'x' + height + '!');
587      }
588      var src = {};
589      if (width / destSize.width > height / destSize.height) {
590        // Full height, crop left/right.
591        src.height = height;
592        src.width = height * destSize.width / destSize.height;
593      } else {
594        // Full width, crop top/bottom.
595        src.width = width;
596        src.height = width * destSize.height / destSize.width;
597      }
598      src.x = (width - src.width) / 2;
599      src.y = (height - src.height) / 2;
600      ctx.drawImage(video, src.x, src.y, src.width, src.height,
601                    0, 0, destSize.width, destSize.height);
602    },
603
604    /**
605     * Flips frame horizontally.
606     * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} source
607     *     Frame to flip.
608     * @return {string} Flipped frame as data URL.
609     */
610    flipFrame_: function(source) {
611      var canvas = document.createElement('canvas');
612      canvas.width = CAPTURE_SIZE.width;
613      canvas.height = CAPTURE_SIZE.height;
614      var ctx = canvas.getContext('2d');
615      ctx.translate(CAPTURE_SIZE.width, 0);
616      ctx.scale(-1.0, 1.0);
617      ctx.drawImage(source, 0, 0);
618      return canvas.toDataURL('image/png');
619    },
620
621    /**
622     * Capture next frame of the video being recorded after a takeVideo() call.
623     * @param {Object} data Property bag with the recorder details.
624     * @private
625     */
626    captureVideoFrame_: function(data) {
627      var lastTimestamp = new Date().getTime();
628      var delayMs = lastTimestamp - data.lastTimestamp;
629      console.error('Delay: ' + delayMs + ' (' + (1000 / delayMs + ' FPS)'));
630      data.lastTimestamp = lastTimestamp;
631
632      this.captureFrame_(this.cameraVideo_, data.ctx, CAPTURE_SIZE);
633      data.ctx.translate(0, CAPTURE_SIZE.height);
634
635      if (++data.frameNo == RECORD_FRAMES) {
636        window.clearTimeout(data.timer);
637        if (data.callback && typeof data.callback == 'function')
638          data.callback(data.canvas.toDataURL('image/png'));
639      }
640    },
641
642    /**
643     * Adds new image to the user image grid.
644     * @param {string} src Image URL.
645     * @param {string=} opt_title Image tooltip.
646     * @param {function=} opt_clickHandler Image click handler.
647     * @param {number=} opt_position If given, inserts new image into
648     *     that position (0-based) in image list.
649     * @param {function=} opt_decorateFn Function called with the list element
650     *     as argument to do any final decoration.
651     * @return {!Object} Image data inserted into the data model.
652     */
653    // TODO(ivankr): this function needs some argument list refactoring.
654    addItem: function(url, opt_title, opt_clickHandler, opt_position,
655                      opt_decorateFn) {
656      var imageInfo = {
657        url: url,
658        title: opt_title,
659        clickHandler: opt_clickHandler,
660        decorateFn: opt_decorateFn
661      };
662      this.inProgramSelection_ = true;
663      if (opt_position !== undefined)
664        this.dataModel.splice(opt_position, 0, imageInfo);
665      else
666        this.dataModel.push(imageInfo);
667      this.inProgramSelection_ = false;
668      return imageInfo;
669    },
670
671    /**
672     * Returns index of an image in grid.
673     * @param {Object} imageInfo Image data returned from addItem() call.
674     * @return {number} Image index (0-based) or -1 if image was not found.
675     */
676    indexOf: function(imageInfo) {
677      return this.dataModel.indexOf(imageInfo);
678    },
679
680    /**
681     * Replaces an image in the grid.
682     * @param {Object} imageInfo Image data returned from addItem() call.
683     * @param {string} imageUrl New image URL.
684     * @param {string=} opt_title New image tooltip (if undefined, tooltip
685     *     is left unchanged).
686     * @return {!Object} Image data of the added or updated image.
687     */
688    updateItem: function(imageInfo, imageUrl, opt_title) {
689      var imageIndex = this.indexOf(imageInfo);
690      var wasSelected = this.selectionModel.selectedIndex == imageIndex;
691      this.removeItem(imageInfo);
692      var newInfo = this.addItem(
693          imageUrl,
694          opt_title === undefined ? imageInfo.title : opt_title,
695          imageInfo.clickHandler,
696          imageIndex,
697          imageInfo.decorateFn);
698      // Update image data with the reset of the keys from the old data.
699      for (k in imageInfo) {
700        if (!(k in newInfo))
701          newInfo[k] = imageInfo[k];
702      }
703      if (wasSelected)
704        this.selectedItem = newInfo;
705      return newInfo;
706    },
707
708    /**
709     * Removes previously added image from the grid.
710     * @param {Object} imageInfo Image data returned from the addItem() call.
711     */
712    removeItem: function(imageInfo) {
713      var index = this.indexOf(imageInfo);
714      if (index != -1) {
715        var wasSelected = this.selectionModel.selectedIndex == index;
716        this.inProgramSelection_ = true;
717        this.dataModel.splice(index, 1);
718        if (wasSelected) {
719          // If item removed was selected, select the item next to it.
720          this.selectedItem = this.dataModel.item(
721              Math.min(this.dataModel.length - 1, index));
722        }
723        this.inProgramSelection_ = false;
724      }
725    },
726
727    /**
728     * Forces re-display, size re-calculation and focuses grid.
729     */
730    updateAndFocus: function() {
731      // Recalculate the measured item size.
732      this.measured_ = null;
733      this.columns = 0;
734      this.redraw();
735      this.focus();
736    }
737  };
738
739  /**
740   * URLs of special button images.
741   * @enum {string}
742   */
743  UserImagesGrid.ButtonImages = {
744    TAKE_PHOTO: 'chrome://theme/IDR_BUTTON_USER_IMAGE_TAKE_PHOTO',
745    CHOOSE_FILE: 'chrome://theme/IDR_BUTTON_USER_IMAGE_CHOOSE_FILE',
746    PROFILE_PICTURE: 'chrome://theme/IDR_PROFILE_PICTURE_LOADING'
747  };
748
749  return {
750    UserImagesGrid: UserImagesGrid
751  };
752});
753