• Home
  • Line#
  • Scopes#
  • Navigate#
  • Raw
  • Download
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
5cr.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});
397