Merge Chromium + Blink git repositories
[chromium-blink-merge.git] / chrome / browser / resources / chromeos / wallpaper_manager / js / wallpaper_images_grid.js
blob8b5926842c176dd405e67b4253e9924be8767d91
1 // Copyright (c) 2013 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('wallpapers', 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   /** @const */ var ThumbnailSuffix = '_thumbnail.png';
12   /** @const */ var ShowSpinnerDelayMs = 500;
14   /**
15    * Creates a new wallpaper thumbnails grid item.
16    * @param {{wallpaperId: number, baseURL: string, layout: string,
17    *          source: string, availableOffline: boolean,
18    *          opt_dynamicURL: string, opt_author: string,
19    *          opt_authorWebsite: string}}
20    *     wallpaperInfo Wallpaper data item in WallpaperThumbnailsGrid's data
21    *     model.
22    * @param {number} dataModelId A unique ID that this item associated to.
23    * @param {object} thumbnail The thumbnail image Object associated with this
24    *      grid item.
25    * @param {function} callback The callback function when decoration finished.
26    * @constructor
27    * @extends {cr.ui.GridItem}
28    */
29   function WallpaperThumbnailsGridItem(wallpaperInfo,
30                                        dataModelId,
31                                        thumbnail,
32                                        callback) {
33     var el = new GridItem(wallpaperInfo);
34     el.__proto__ = WallpaperThumbnailsGridItem.prototype;
35     el.dataModelId_ = dataModelId;
36     el.thumbnail_ = thumbnail;
37     el.callback_ = callback;
38     return el;
39   }
41   WallpaperThumbnailsGridItem.prototype = {
42     __proto__: GridItem.prototype,
44     /**
45      * The unique ID this thumbnail grid associated to.
46      * @type {number}
47      */
48     dataModelId_: null,
50     /**
51      * The thumbnail image associated with the current grid item.
52      */
53     thumbnail_: null,
55     /**
56      * Called when the WallpaperThumbnailsGridItem is decorated or failed to
57      * decorate. If the decoration contains image, the callback function should
58      * be called after image loaded.
59      * @type {function}
60      */
61     callback_: null,
63     /** @override */
64     decorate: function() {
65       GridItem.prototype.decorate.call(this);
66       // Removes garbage created by GridItem.
67       this.innerText = '';
69       if (this.thumbnail_) {
70         this.appendChild(this.thumbnail_);
71         this.callback_(this.dataModelId_);
72         return;
73       }
75       var imageEl = cr.doc.createElement('img');
76       imageEl.classList.add('thumbnail');
77       cr.defineProperty(imageEl, 'offline', cr.PropertyKind.BOOL_ATTR);
78       imageEl.offline = this.dataItem.availableOffline;
79       this.appendChild(imageEl);
80       var self = this;
82       switch (this.dataItem.source) {
83         case Constants.WallpaperSourceEnum.AddNew:
84           this.id = 'add-new';
85           this.addEventListener('click', function(e) {
86             var checkbox = $('surprise-me').querySelector('#checkbox');
87             if (!checkbox.classList.contains('checked'))
88               $('wallpaper-selection-container').hidden = false;
89           });
90           // Delay dispatching the completion callback until all items have
91           // begun loading and are tracked.
92           window.setTimeout(this.callback_.bind(this, this.dataModelId_), 0);
93           break;
94         case Constants.WallpaperSourceEnum.Custom:
95           var errorHandler = function(e) {
96             self.callback_(self.dataModelId_);
97             console.error('Can not access file system.');
98           };
99           var wallpaperDirectories = WallpaperDirectories.getInstance();
100           var getThumbnail = function(fileName) {
101             var setURL = function(fileEntry) {
102               imageEl.src = fileEntry.toURL();
103               self.callback_(self.dataModelId_,
104                              self.dataItem.wallpaperId,
105                              imageEl);
106             };
107             var fallback = function() {
108               wallpaperDirectories.getDirectory(
109                   Constants.WallpaperDirNameEnum.ORIGINAL, function(dirEntry) {
110                 dirEntry.getFile(fileName, {create: false}, setURL,
111                                  errorHandler);
112               }, errorHandler);
113             };
114             var success = function(dirEntry) {
115               dirEntry.getFile(fileName, {create: false}, setURL, fallback);
116             };
117             wallpaperDirectories.getDirectory(
118                Constants.WallpaperDirNameEnum.THUMBNAIL, success, errorHandler);
119           };
120           getThumbnail(self.dataItem.baseURL);
121           break;
122         case Constants.WallpaperSourceEnum.OEM:
123         case Constants.WallpaperSourceEnum.Online:
124           chrome.wallpaperPrivate.getThumbnail(this.dataItem.baseURL,
125                                                this.dataItem.source,
126                                                function(data) {
127             if (data) {
128               var blob = new Blob([new Int8Array(data)],
129                                   {'type': 'image\/png'});
130               imageEl.src = window.URL.createObjectURL(blob);
131               imageEl.addEventListener('load', function(e) {
132                 self.callback_(self.dataModelId_,
133                                self.dataItem.wallpaperId,
134                                imageEl);
135                 window.URL.revokeObjectURL(this.src);
136               });
137             } else if (self.dataItem.source ==
138                        Constants.WallpaperSourceEnum.Online) {
139               var xhr = new XMLHttpRequest();
140               xhr.open('GET', self.dataItem.baseURL + ThumbnailSuffix, true);
141               xhr.responseType = 'arraybuffer';
142               xhr.send(null);
143               xhr.addEventListener('load', function(e) {
144                 if (xhr.status === 200) {
145                   chrome.wallpaperPrivate.saveThumbnail(self.dataItem.baseURL,
146                                                         xhr.response);
147                   var blob = new Blob([new Int8Array(xhr.response)],
148                                       {'type' : 'image\/png'});
149                   imageEl.src = window.URL.createObjectURL(blob);
150                   // TODO(bshe): We currently use empty div to reserve space for
151                   // thumbnail. Use a placeholder like "loading" image may
152                   // better.
153                   imageEl.addEventListener('load', function(e) {
154                     self.callback_(self.dataModelId_,
155                                    self.dataItem.wallpaperId,
156                                    this);
157                     window.URL.revokeObjectURL(this.src);
158                   });
159                 } else {
160                   self.callback_(self.dataModelId_);
161                 }
162               });
163             }
164           });
165           break;
166         default:
167           console.error('Unsupported image source.');
168           // Delay dispatching the completion callback until all items have
169           // begun loading and are tracked.
170           window.setTimeout(this.callback_.bind(this, this.dataModelId_), 0);
171       }
172     },
173   };
175   /**
176    * Creates a selection controller that wraps selection on grid ends
177    * and translates Enter presses into 'activate' events.
178    * @param {cr.ui.ListSelectionModel} selectionModel The selection model to
179    *     interact with.
180    * @param {cr.ui.Grid} grid The grid to interact with.
181    * @constructor
182    * @extends {cr.ui.GridSelectionController}
183    */
184   function WallpaperThumbnailsGridSelectionController(selectionModel, grid) {
185     GridSelectionController.call(this, selectionModel, grid);
186   }
188   WallpaperThumbnailsGridSelectionController.prototype = {
189     __proto__: GridSelectionController.prototype,
191     /** @override */
192     getIndexBefore: function(index) {
193       var result =
194           GridSelectionController.prototype.getIndexBefore.call(this, index);
195       return result == -1 ? this.getLastIndex() : result;
196     },
198     /** @override */
199     getIndexAfter: function(index) {
200       var result =
201           GridSelectionController.prototype.getIndexAfter.call(this, index);
202       return result == -1 ? this.getFirstIndex() : result;
203     },
205     /** @override */
206     handleKeyDown: function(e) {
207       if (e.keyIdentifier == 'Enter')
208         cr.dispatchSimpleEvent(this.grid_, 'activate');
209       else
210         GridSelectionController.prototype.handleKeyDown.call(this, e);
211     },
212   };
214   /**
215    * Creates a new user images grid element.
216    * @param {Object=} opt_propertyBag Optional properties.
217    * @constructor
218    * @extends {cr.ui.Grid}
219    */
220   var WallpaperThumbnailsGrid = cr.ui.define('grid');
222   WallpaperThumbnailsGrid.prototype = {
223     __proto__: Grid.prototype,
225     /**
226      * The checkbox element.
227      */
228     checkmark_: undefined,
230     /**
231      * ID of spinner delay timer.
232      * @private
233      */
234     spinnerTimeout_: 0,
236     /**
237      * The item in data model which should have a checkmark.
238      * @type {{baseURL: string, dynamicURL: string, layout: string,
239      *         author: string, authorWebsite: string,
240      *         availableOffline: boolean}}
241      *     wallpaperInfo The information of the wallpaper to be set active.
242      */
243     activeItem_: undefined,
244     set activeItem(activeItem) {
245       if (this.activeItem_ != activeItem) {
246         this.activeItem_ = activeItem;
247         this.updateActiveThumb_();
248       }
249     },
251     get activeItem() {
252       return this.activeItem_;
253     },
255     /**
256      * A unique ID that assigned to each set dataModel operation. Note that this
257      * id wont increase if the new dataModel is null or empty.
258      */
259     dataModelId_: 0,
261     /**
262      * The number of items that need to be generated after a new dataModel is
263      * set.
264      */
265     pendingItems_: 0,
267     /**
268      * Maintains all grid items' thumbnail images for quickly switching between
269      * different categories.
270      */
271     thumbnailList_: undefined,
273     /** @override */
274     set dataModel(dataModel) {
275       if (this.dataModel_ == dataModel)
276         return;
278       if (dataModel && dataModel.length != 0) {
279         this.dataModelId_++;
280         // Clears old pending items. The new pending items will be counted when
281         // item is constructed in function itemConstructor below.
282         this.pendingItems_ = 0;
284         this.style.visibility = 'hidden';
285         // If spinner is hidden, schedule to show the spinner after
286         // ShowSpinnerDelayMs delay. Otherwise, keep it spinning.
287         if ($('spinner-container').hidden) {
288           this.spinnerTimeout_ = window.setTimeout(function() {
289             $('spinner-container').hidden = false;
290           }, ShowSpinnerDelayMs);
291         }
292       } else {
293         // Sets dataModel to null should hide spinner immediately.
294         $('spinner-container').hidden = true;
295       }
297       var parentSetter = cr.ui.Grid.prototype.__lookupSetter__('dataModel');
298       parentSetter.call(this, dataModel);
299     },
301     get dataModel() {
302       return this.dataModel_;
303     },
305     /** @override */
306     createSelectionController: function(sm) {
307       return new WallpaperThumbnailsGridSelectionController(sm, this);
308     },
310     /**
311      * Check if new thumbnail grid finished loading. This reduces the count of
312      * remaining items to be loaded and when 0, shows the thumbnail grid. Note
313      * it does not reduce the count on a previous |dataModelId|.
314      * @param {number} dataModelId A unique ID that a thumbnail item is
315      *     associated to.
316      * @param {number} opt_wallpaperId The unique wallpaper ID that associated
317      *     with this thumbnail gird item.
318      * @param {object} opt_thumbnail The thumbnail image that associated with
319      *     the opt_wallpaperId.
320      */
321     pendingItemComplete: function(dataModelId,
322                                   opt_wallpaperId,
323                                   opt_thumbnail) {
324       if (dataModelId != this.dataModelId_)
325         return;
326       this.pendingItems_--;
327       if (opt_wallpaperId != null)
328         this.thumbnailList_[opt_wallpaperId] = opt_thumbnail;
329       if (this.pendingItems_ == 0) {
330         this.style.visibility = 'visible';
331         window.clearTimeout(this.spinnerTimeout_);
332         this.spinnerTimeout_ = 0;
333         $('spinner-container').hidden = true;
334       }
335     },
337     /** @override */
338     decorate: function() {
339       Grid.prototype.decorate.call(this);
340       // checkmark_ needs to be initialized before set data model. Otherwise, we
341       // may try to access checkmark before initialization in
342       // updateActiveThumb_().
343       this.checkmark_ = cr.doc.createElement('div');
344       this.checkmark_.classList.add('check');
345       this.dataModel = new ArrayDataModel([]);
346       this.thumbnailList_ = new ArrayDataModel([]);
347       var self = this;
348       this.itemConstructor = function(value) {
349         var dataModelId = self.dataModelId_;
350         self.pendingItems_++;
351         return WallpaperThumbnailsGridItem(value, dataModelId,
352             (value.wallpaperId == null) ?
353                 null : self.thumbnailList_[value.wallpaperId],
354             self.pendingItemComplete.bind(self));
355       };
356       this.selectionModel = new ListSingleSelectionModel();
357       this.inProgramSelection_ = false;
358     },
360     /**
361      * Should only be queried from the 'change' event listener, true if the
362      * change event was triggered by a programmatical selection change.
363      * @type {boolean}
364      */
365     get inProgramSelection() {
366       return this.inProgramSelection_;
367     },
369     /**
370      * Set index to the image selected.
371      * @type {number} index The index of selected image.
372      */
373     set selectedItemIndex(index) {
374       this.inProgramSelection_ = true;
375       this.selectionModel.selectedIndex = index;
376       this.inProgramSelection_ = false;
377     },
379     /**
380      * The selected item.
381      * @type {!Object} Wallpaper information inserted into the data model.
382      */
383     get selectedItem() {
384       var index = this.selectionModel.selectedIndex;
385       return index != -1 ? this.dataModel.item(index) : null;
386     },
387     set selectedItem(selectedItem) {
388       var index = this.dataModel.indexOf(selectedItem);
389       this.inProgramSelection_ = true;
390       this.selectionModel.leadIndex = index;
391       this.selectionModel.selectedIndex = index;
392       this.inProgramSelection_ = false;
393     },
395     /**
396      * Forces re-display, size re-calculation and focuses grid.
397      */
398     updateAndFocus: function() {
399       // Recalculate the measured item size.
400       this.measured_ = null;
401       this.columns = 0;
402       this.redraw();
403       this.focus();
404     },
406     /**
407      * Shows a checkmark on the active thumbnail and clears previous active one
408      * if any. Note if wallpaper was not set successfully, checkmark should not
409      * show on that thumbnail.
410      */
411     updateActiveThumb_: function() {
412       var selectedGridItem = this.getListItem(this.activeItem_);
413       if (this.checkmark_.parentNode &&
414           this.checkmark_.parentNode == selectedGridItem) {
415         return;
416       }
418       // Clears previous checkmark.
419       if (this.checkmark_.parentNode)
420         this.checkmark_.parentNode.removeChild(this.checkmark_);
422       if (!selectedGridItem)
423         return;
424       selectedGridItem.appendChild(this.checkmark_);
425     },
427     /**
428      * Redraws the viewport.
429      */
430     redraw: function() {
431       Grid.prototype.redraw.call(this);
432       // The active thumbnail maybe deleted in the above redraw(). Sets it again
433       // to make sure checkmark shows correctly.
434       this.updateActiveThumb_();
435     }
436   };
438   return {
439     WallpaperThumbnailsGrid: WallpaperThumbnailsGrid
440   };