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