Pin Chrome's shortcut to the Win10 Start menu on install and OS upgrade.
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / user_images_grid.js
blobaaecf7af9646db9605a69a1977b4869ffb99c5c5
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     * Dimensions for camera capture.
14     * @const
15     */
16   var CAPTURE_SIZE = {
17     height: 480,
18     width: 480
19   };
21   /**
22    * Path for internal URLs.
23    * @const
24    */
25   var CHROME_THEME_PATH = 'chrome://theme';
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   }
42   UserImagesGridItem.prototype = {
43     __proto__: GridItem.prototype,
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       imageEl.alt = imageEl.title;
58       if (typeof this.dataItem.clickHandler == 'function')
59         imageEl.addEventListener('mousedown', this.dataItem.clickHandler);
60       // Remove any garbage added by GridItem and ListItem decorators.
61       this.textContent = '';
62       this.appendChild(imageEl);
63       if (typeof this.dataItem.decorateFn == 'function')
64         this.dataItem.decorateFn(this);
65       this.setAttribute('role', 'option');
66       this.oncontextmenu = function(e) { e.preventDefault(); };
67     }
68   };
70   /**
71    * Creates a selection controller that wraps selection on grid ends
72    * and translates Enter presses into 'activate' events.
73    * @param {cr.ui.ListSelectionModel} selectionModel The selection model to
74    *     interact with.
75    * @param {cr.ui.Grid} grid The grid to interact with.
76    * @constructor
77    * @extends {cr.ui.GridSelectionController}
78    */
79   function UserImagesGridSelectionController(selectionModel, grid) {
80     GridSelectionController.call(this, selectionModel, grid);
81   }
83   UserImagesGridSelectionController.prototype = {
84     __proto__: GridSelectionController.prototype,
86     /** @override */
87     getIndexBefore: function(index) {
88       var result =
89           GridSelectionController.prototype.getIndexBefore.call(this, index);
90       return result == -1 ? this.getLastIndex() : result;
91     },
93     /** @override */
94     getIndexAfter: function(index) {
95       var result =
96           GridSelectionController.prototype.getIndexAfter.call(this, index);
97       return result == -1 ? this.getFirstIndex() : result;
98     },
100     /** @override */
101     handleKeyDown: function(e) {
102       if (e.keyIdentifier == 'Enter')
103         cr.dispatchSimpleEvent(this.grid_, 'activate');
104       else
105         GridSelectionController.prototype.handleKeyDown.call(this, e);
106     }
107   };
109   /**
110    * Creates a new user images grid element.
111    * @param {Object=} opt_propertyBag Optional properties.
112    * @constructor
113    * @extends {cr.ui.Grid}
114    */
115   var UserImagesGrid = cr.ui.define('grid');
117   UserImagesGrid.prototype = {
118     __proto__: Grid.prototype,
120     /** @override */
121     createSelectionController: function(sm) {
122       return new UserImagesGridSelectionController(sm, this);
123     },
125     /** @override */
126     decorate: function() {
127       Grid.prototype.decorate.call(this);
128       this.dataModel = new ArrayDataModel([]);
129       this.itemConstructor = /** @type {function(new:cr.ui.ListItem, *)} */(
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     },
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     },
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;
160       var oldSelectionType = this.selectionType;
162       // Update current selection type.
163       this.selectionType = this.selectedItem.type;
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');
169       this.updatePreview_();
171       var e = new Event('select');
172       e.oldSelectionType = oldSelectionType;
173       this.dispatchEvent(e);
174     },
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     },
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     },
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     },
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     },
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     },
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      * @suppress {deprecated}
250      */
251     handleCameraAvailable_: function(onAvailable, stream) {
252       if (this.cameraStartInProgress_ && onAvailable()) {
253         this.cameraVideo_.src = URL.createObjectURL(stream);
254         this.cameraStream_ = stream;
255       } else {
256         stream.stop();
257       }
258       this.cameraStartInProgress_ = false;
259     },
261     /**
262      * Handles camera check failure.
263      * @param {NavigatorUserMediaError=} err Error object.
264      * @private
265      */
266     handleCameraAbsent_: function(err) {
267       this.cameraPresent = false;
268       this.cameraOnline = false;
269       this.cameraStartInProgress_ = false;
270     },
272     /**
273      * Handles successful camera capture start.
274      * @private
275      */
276     handleVideoStarted_: function() {
277       this.cameraOnline = true;
278       this.handleVideoUpdate_();
279     },
281     /**
282      * Handles camera stream update. Called regularly (at rate no greater then
283      * 4/sec) while camera stream is live.
284      * @private
285      */
286     handleVideoUpdate_: function() {
287       this.lastFrameTime_ = new Date().getTime();
288     },
290     /**
291      * Type of the selected image (one of 'default', 'profile', 'camera').
292      * Setting it will update class list of |previewElement|.
293      * @type {string}
294      */
295     get selectionType() {
296       return this.selectionType_;
297     },
298     set selectionType(value) {
299       this.selectionType_ = value;
300       var previewClassList = this.previewElement.classList;
301       previewClassList[value == 'default' ? 'add' : 'remove']('default-image');
302       previewClassList[value == 'profile' ? 'add' : 'remove']('profile-image');
303       previewClassList[value == 'camera' ? 'add' : 'remove']('camera');
305       var setFocusIfLost = function() {
306         // Set focus to the grid, if focus is not on UI.
307         if (!document.activeElement ||
308             document.activeElement.tagName == 'BODY') {
309           $('user-image-grid').focus();
310         }
311       };
312       // Timeout guarantees processing AFTER style changes display attribute.
313       setTimeout(setFocusIfLost, 0);
314     },
316     /**
317      * Current image captured from camera as data URL. Setting to null will
318      * return to the live camera stream.
319      * @type {(string|undefined)}
320      */
321     get cameraImage() {
322       return this.cameraImage_;
323     },
324     set cameraImage(imageUrl) {
325       this.cameraLive = !imageUrl;
326       if (this.cameraPresent && !imageUrl)
327         imageUrl = UserImagesGrid.ButtonImages.TAKE_PHOTO;
328       if (imageUrl) {
329         this.cameraImage_ = this.cameraImage_ ?
330             this.updateItem(this.cameraImage_, imageUrl, this.cameraTitle_) :
331             this.addItem(imageUrl, this.cameraTitle_, undefined, 0);
332         this.cameraImage_.type = 'camera';
333       } else {
334         this.removeItem(this.cameraImage_);
335         this.cameraImage_ = null;
336       }
337     },
339     /**
340      * Updates the titles for the camera element.
341      * @param {string} placeholderTitle Title when showing a placeholder.
342      * @param {string} capturedImageTitle Title when showing a captured photo.
343      */
344     setCameraTitles: function(placeholderTitle, capturedImageTitle) {
345       this.placeholderTitle_ = placeholderTitle;
346       this.capturedImageTitle_ = capturedImageTitle;
347       this.cameraTitle_ = this.placeholderTitle_;
348     },
350     /**
351      * True when camera is in live mode (i.e. no still photo selected).
352      * @type {boolean}
353      */
354     get cameraLive() {
355       return this.cameraLive_;
356     },
357     set cameraLive(value) {
358       this.cameraLive_ = value;
359       this.previewElement.classList[value ? 'add' : 'remove']('live');
360     },
362     /**
363      * Should only be queried from the 'change' event listener, true if the
364      * change event was triggered by a programmatical selection change.
365      * @type {boolean}
366      */
367     get inProgramSelection() {
368       return this.inProgramSelection_;
369     },
371     /**
372      * URL of the image selected.
373      * @type {string?}
374      */
375     get selectedItemUrl() {
376       var selectedItem = this.selectedItem;
377       return selectedItem ? selectedItem.url : null;
378     },
379     set selectedItemUrl(url) {
380       for (var i = 0, el; el = this.dataModel.item(i); i++) {
381         if (el.url === url)
382           this.selectedItemIndex = i;
383       }
384     },
386     /**
387      * Set index to the image selected.
388      * @type {number} index The index of selected image.
389      */
390     set selectedItemIndex(index) {
391       this.inProgramSelection_ = true;
392       this.selectionModel.selectedIndex = index;
393       this.inProgramSelection_ = false;
394     },
396     /** @override */
397     get selectedItem() {
398       var index = this.selectionModel.selectedIndex;
399       return index != -1 ? this.dataModel.item(index) : null;
400     },
401     set selectedItem(selectedItem) {
402       var index = this.indexOf(selectedItem);
403       this.inProgramSelection_ = true;
404       this.selectionModel.selectedIndex = index;
405       this.selectionModel.leadIndex = index;
406       this.inProgramSelection_ = false;
407     },
409     /**
410      * Element containing the preview image (the first IMG element) and the
411      * camera live stream (the first VIDEO element).
412      * @type {HTMLElement}
413      */
414     get previewElement() {
415       // TODO(ivankr): temporary hack for non-HTML5 version.
416       return this.previewElement_ || this;
417     },
418     set previewElement(value) {
419       this.previewElement_ = value;
420       this.previewImage_ = value.querySelector('img');
421       this.cameraVideo_ = value.querySelector('video');
422       this.cameraVideo_.addEventListener('canplay',
423                                          this.handleVideoStarted_.bind(this));
424       this.cameraVideo_.addEventListener('timeupdate',
425                                          this.handleVideoUpdate_.bind(this));
426       this.updatePreview_();
427       // Initialize camera state and check for its presence.
428       this.cameraLive = true;
429       this.cameraPresent = false;
430     },
432     /**
433      * Whether the camera live stream and photo should be flipped horizontally.
434      * If setting this property results in photo update, 'photoupdated' event
435      * will be fired with 'dataURL' property containing the photo encoded as
436      * a data URL
437      * @type {boolean}
438      */
439     get flipPhoto() {
440       return this.flipPhoto_ || false;
441     },
442     set flipPhoto(value) {
443       if (this.flipPhoto_ == value)
444         return;
445       this.flipPhoto_ = value;
446       this.previewElement.classList.toggle('flip-x', value);
447       /* TODO(merkulova): remove when webkit crbug.com/126479 is fixed. */
448       this.flipPhotoElement.classList.toggle('flip-trick', value);
449       if (!this.cameraLive) {
450         // Flip current still photo.
451         var e = new Event('photoupdated');
452         e.dataURL = this.flipPhoto ?
453             this.flipFrame_(this.previewImage_) : this.previewImage_.src;
454         this.dispatchEvent(e);
455       }
456     },
458     /**
459      * Performs photo capture from the live camera stream. 'phototaken' event
460      * will be fired as soon as captured photo is available, with 'dataURL'
461      * property containing the photo encoded as a data URL.
462      * @return {boolean} Whether photo capture was successful.
463      */
464     takePhoto: function() {
465       if (!this.cameraOnline)
466         return false;
467       var canvas = /** @type {HTMLCanvasElement} */(
468           document.createElement('canvas'));
469       canvas.width = CAPTURE_SIZE.width;
470       canvas.height = CAPTURE_SIZE.height;
471       this.captureFrame_(
472           this.cameraVideo_,
473           /** @type {CanvasRenderingContext2D} */(canvas.getContext('2d')),
474           CAPTURE_SIZE);
475       // Preload image before displaying it.
476       var previewImg = new Image();
477       previewImg.addEventListener('load', function(e) {
478         this.cameraTitle_ = this.capturedImageTitle_;
479         this.cameraImage = previewImg.src;
480       }.bind(this));
481       previewImg.src = canvas.toDataURL('image/png');
482       var e = new Event('phototaken');
483       e.dataURL = this.flipPhoto ? this.flipFrame_(canvas) : previewImg.src;
484       this.dispatchEvent(e);
485       return true;
486     },
488     /**
489      * Discard current photo and return to the live camera stream.
490      */
491     discardPhoto: function() {
492       this.cameraTitle_ = this.placeholderTitle_;
493       this.cameraImage = null;
494     },
496     /**
497      * Capture a single still frame from a <video> element, placing it at the
498      * current drawing origin of a canvas context.
499      * @param {HTMLVideoElement} video Video element to capture from.
500      * @param {CanvasRenderingContext2D} ctx Canvas context to draw onto.
501      * @param {{width: number, height: number}} destSize Capture size.
502      * @private
503      */
504     captureFrame_: function(video, ctx, destSize) {
505       var width = video.videoWidth;
506       var height = video.videoHeight;
507       if (width < destSize.width || height < destSize.height) {
508         console.error('Video capture size too small: ' +
509                       width + 'x' + height + '!');
510       }
511       var src = {};
512       if (width / destSize.width > height / destSize.height) {
513         // Full height, crop left/right.
514         src.height = height;
515         src.width = height * destSize.width / destSize.height;
516       } else {
517         // Full width, crop top/bottom.
518         src.width = width;
519         src.height = width * destSize.height / destSize.width;
520       }
521       src.x = (width - src.width) / 2;
522       src.y = (height - src.height) / 2;
523       ctx.drawImage(video, src.x, src.y, src.width, src.height,
524                     0, 0, destSize.width, destSize.height);
525     },
527     /**
528      * Flips frame horizontally.
529      * @param {HTMLImageElement|HTMLCanvasElement|HTMLVideoElement} source
530      *     Frame to flip.
531      * @return {string} Flipped frame as data URL.
532      */
533     flipFrame_: function(source) {
534       var canvas = document.createElement('canvas');
535       canvas.width = CAPTURE_SIZE.width;
536       canvas.height = CAPTURE_SIZE.height;
537       var ctx = canvas.getContext('2d');
538       ctx.translate(CAPTURE_SIZE.width, 0);
539       ctx.scale(-1.0, 1.0);
540       ctx.drawImage(source, 0, 0);
541       return canvas.toDataURL('image/png');
542     },
544     /**
545      * Adds new image to the user image grid.
546      * @param {string} url Image URL.
547      * @param {string=} opt_title Image tooltip.
548      * @param {Function=} opt_clickHandler Image click handler.
549      * @param {number=} opt_position If given, inserts new image into
550      *     that position (0-based) in image list.
551      * @param {Function=} opt_decorateFn Function called with the list element
552      *     as argument to do any final decoration.
553      * @return {!Object} Image data inserted into the data model.
554      */
555     // TODO(ivankr): this function needs some argument list refactoring.
556     addItem: function(url, opt_title, opt_clickHandler, opt_position,
557                       opt_decorateFn) {
558       var imageInfo = {
559         url: url,
560         title: opt_title,
561         clickHandler: opt_clickHandler,
562         decorateFn: opt_decorateFn
563       };
564       this.inProgramSelection_ = true;
565       if (opt_position !== undefined)
566         this.dataModel.splice(opt_position, 0, imageInfo);
567       else
568         this.dataModel.push(imageInfo);
569       this.inProgramSelection_ = false;
570       return imageInfo;
571     },
573     /**
574      * Returns index of an image in grid.
575      * @param {Object} imageInfo Image data returned from addItem() call.
576      * @return {number} Image index (0-based) or -1 if image was not found.
577      */
578     indexOf: function(imageInfo) {
579       return this.dataModel.indexOf(imageInfo);
580     },
582     /**
583      * Replaces an image in the grid.
584      * @param {Object} imageInfo Image data returned from addItem() call.
585      * @param {string} imageUrl New image URL.
586      * @param {string=} opt_title New image tooltip (if undefined, tooltip
587      *     is left unchanged).
588      * @return {!Object} Image data of the added or updated image.
589      */
590     updateItem: function(imageInfo, imageUrl, opt_title) {
591       var imageIndex = this.indexOf(imageInfo);
592       var wasSelected = this.selectionModel.selectedIndex == imageIndex;
593       this.removeItem(imageInfo);
594       var newInfo = this.addItem(
595           imageUrl,
596           opt_title === undefined ? imageInfo.title : opt_title,
597           imageInfo.clickHandler,
598           imageIndex,
599           imageInfo.decorateFn);
600       // Update image data with the reset of the keys from the old data.
601       for (var k in imageInfo) {
602         if (!(k in newInfo))
603           newInfo[k] = imageInfo[k];
604       }
605       if (wasSelected)
606         this.selectedItem = newInfo;
607       return newInfo;
608     },
610     /**
611      * Removes previously added image from the grid.
612      * @param {Object} imageInfo Image data returned from the addItem() call.
613      */
614     removeItem: function(imageInfo) {
615       var index = this.indexOf(imageInfo);
616       if (index != -1) {
617         var wasSelected = this.selectionModel.selectedIndex == index;
618         this.inProgramSelection_ = true;
619         this.dataModel.splice(index, 1);
620         if (wasSelected) {
621           // If item removed was selected, select the item next to it.
622           this.selectedItem = this.dataModel.item(
623               Math.min(this.dataModel.length - 1, index));
624         }
625         this.inProgramSelection_ = false;
626       }
627     },
629     /**
630      * Forces re-display, size re-calculation and focuses grid.
631      */
632     updateAndFocus: function() {
633       // Recalculate the measured item size.
634       this.measured_ = null;
635       this.columns = 0;
636       this.redraw();
637       this.focus();
638     },
640     /**
641      * Appends default images to the image grid. Should only be called once.
642      * @param {Array<{url: string, author: string,
643      *                website: string, title: string}>} imagesData
644      *   An array of default images data, including URL, author, title and
645      *   website.
646      */
647     setDefaultImages: function(imagesData) {
648       for (var i = 0, data; data = imagesData[i]; i++) {
649         var item = this.addItem(data.url, data.title);
650         item.type = 'default';
651         item.author = data.author || '';
652         item.website = data.website || '';
653       }
654     }
655   };
657   /**
658    * URLs of special button images.
659    * @enum {string}
660    */
661   UserImagesGrid.ButtonImages = {
662     TAKE_PHOTO: 'chrome://theme/IDR_BUTTON_USER_IMAGE_TAKE_PHOTO',
663     CHOOSE_FILE: 'chrome://theme/IDR_BUTTON_USER_IMAGE_CHOOSE_FILE',
664     PROFILE_PICTURE: 'chrome://theme/IDR_PROFILE_PICTURE_LOADING'
665   };
667   return {
668     UserImagesGrid: UserImagesGrid
669   };