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