summaryrefslogtreecommitdiff
path: root/chromium/chrome/browser/resources/chromeos/wallpaper_manager/js/wallpaper_images_grid.js
blob: a473875bc0f15efc55a47235b956953148d20c5a (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
// Copyright (c) 2013 The Chromium Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

cr.define('wallpapers', function() {
  /** @const */ var ArrayDataModel = cr.ui.ArrayDataModel;
  /** @const */ var Grid = cr.ui.Grid;
  /** @const */ var GridItem = cr.ui.GridItem;
  /** @const */ var GridSelectionController = cr.ui.GridSelectionController;
  /** @const */ var ListSingleSelectionModel = cr.ui.ListSingleSelectionModel;
  /** @const */ var ThumbnailSuffix = '_thumbnail.png';
  /** @const */ var ShowSpinnerDelayMs = 500;

  /**
   * Creates a new wallpaper thumbnails grid item.
   * @param {{wallpaperId: number, baseURL: string, layout: string,
   *          source: string, availableOffline: boolean,
   *          opt_dynamicURL: string, opt_author: string,
   *          opt_authorWebsite: string}}
   *     wallpaperInfo Wallpaper data item in WallpaperThumbnailsGrid's data
   *     model.
   * @param {number} dataModelId A unique ID that this item associated to.
   * @param {object} thumbnail The thumbnail image Object associated with this
   *      grid item.
   * @param {function} callback The callback function when decoration finished.
   * @constructor
   * @extends {cr.ui.GridItem}
   */
  function WallpaperThumbnailsGridItem(wallpaperInfo,
                                       dataModelId,
                                       thumbnail,
                                       callback) {
    var el = new GridItem(wallpaperInfo);
    el.__proto__ = WallpaperThumbnailsGridItem.prototype;
    el.dataModelId_ = dataModelId;
    el.thumbnail_ = thumbnail;
    el.callback_ = callback;
    return el;
  }

  WallpaperThumbnailsGridItem.prototype = {
    __proto__: GridItem.prototype,

    /**
     * The unique ID this thumbnail grid associated to.
     * @type {number}
     */
    dataModelId_: null,

    /**
     * The thumbnail image associated with the current grid item.
     */
    thumbnail_: null,

    /**
     * Called when the WallpaperThumbnailsGridItem is decorated or failed to
     * decorate. If the decoration contains image, the callback function should
     * be called after image loaded.
     * @type {function}
     */
    callback_: null,

    /** @override */
    decorate: function() {
      GridItem.prototype.decorate.call(this);
      // Removes garbage created by GridItem.
      this.innerText = '';

      if (this.thumbnail_) {
        this.appendChild(this.thumbnail_);
        this.callback_(this.dataModelId_);
        return;
      }

      var imageEl = cr.doc.createElement('img');
      imageEl.classList.add('thumbnail');
      cr.defineProperty(imageEl, 'offline', cr.PropertyKind.BOOL_ATTR);
      imageEl.offline = this.dataItem.availableOffline;
      this.appendChild(imageEl);
      var self = this;

      switch (this.dataItem.source) {
        case Constants.WallpaperSourceEnum.AddNew:
          this.id = 'add-new';
          this.addEventListener('click', function(e) {
            var checkbox = $('surprise-me').querySelector('#checkbox');
            if (!checkbox.classList.contains('checked'))
              $('wallpaper-selection-container').hidden = false;
          });
          // Delay dispatching the completion callback until all items have
          // begun loading and are tracked.
          window.setTimeout(this.callback_.bind(this, this.dataModelId_), 0);
          break;
        case Constants.WallpaperSourceEnum.Custom:
          var errorHandler = function(e) {
            self.callback_(self.dataModelId_);
            console.error('Can not access file system.');
          };
          var wallpaperDirectories = WallpaperDirectories.getInstance();
          var getThumbnail = function(fileName) {
            var setURL = function(fileEntry) {
              imageEl.src = fileEntry.toURL();
              self.callback_(self.dataModelId_,
                             self.dataItem.wallpaperId,
                             imageEl);
            };
            var fallback = function() {
              wallpaperDirectories.getDirectory(
                  Constants.WallpaperDirNameEnum.ORIGINAL, function(dirEntry) {
                dirEntry.getFile(fileName, {create: false}, setURL,
                                 errorHandler);
              }, errorHandler);
            };
            var success = function(dirEntry) {
              dirEntry.getFile(fileName, {create: false}, setURL, fallback);
            };
            wallpaperDirectories.getDirectory(
               Constants.WallpaperDirNameEnum.THUMBNAIL, success, errorHandler);
          };
          getThumbnail(self.dataItem.baseURL);
          break;
        case Constants.WallpaperSourceEnum.OEM:
        case Constants.WallpaperSourceEnum.Online:
          chrome.wallpaperPrivate.getThumbnail(this.dataItem.baseURL,
                                               this.dataItem.source,
                                               function(data) {
            if (data) {
              var blob = new Blob([new Int8Array(data)],
                                  {'type': 'image\/png'});
              imageEl.src = window.URL.createObjectURL(blob);
              imageEl.addEventListener('load', function(e) {
                self.callback_(self.dataModelId_,
                               self.dataItem.wallpaperId,
                               imageEl);
                window.URL.revokeObjectURL(this.src);
              });
            } else if (self.dataItem.source ==
                       Constants.WallpaperSourceEnum.Online) {
              var xhr = new XMLHttpRequest();
              xhr.open('GET', self.dataItem.baseURL + ThumbnailSuffix, true);
              xhr.responseType = 'arraybuffer';
              xhr.send(null);
              xhr.addEventListener('load', function(e) {
                if (xhr.status === 200) {
                  chrome.wallpaperPrivate.saveThumbnail(self.dataItem.baseURL,
                                                        xhr.response);
                  var blob = new Blob([new Int8Array(xhr.response)],
                                      {'type' : 'image\/png'});
                  imageEl.src = window.URL.createObjectURL(blob);
                  // TODO(bshe): We currently use empty div to reserve space for
                  // thumbnail. Use a placeholder like "loading" image may
                  // better.
                  imageEl.addEventListener('load', function(e) {
                    self.callback_(self.dataModelId_,
                                   self.dataItem.wallpaperId,
                                   this);
                    window.URL.revokeObjectURL(this.src);
                  });
                } else {
                  self.callback_(self.dataModelId_);
                }
              });
            }
          });
          break;
        default:
          console.error('Unsupported image source.');
          // Delay dispatching the completion callback until all items have
          // begun loading and are tracked.
          window.setTimeout(this.callback_.bind(this, this.dataModelId_), 0);
      }
    },
  };

  /**
   * Creates a selection controller that wraps selection on grid ends
   * and translates Enter presses into 'activate' events.
   * @param {cr.ui.ListSelectionModel} selectionModel The selection model to
   *     interact with.
   * @param {cr.ui.Grid} grid The grid to interact with.
   * @constructor
   * @extends {cr.ui.GridSelectionController}
   */
  function WallpaperThumbnailsGridSelectionController(selectionModel, grid) {
    GridSelectionController.call(this, selectionModel, grid);
  }

  WallpaperThumbnailsGridSelectionController.prototype = {
    __proto__: GridSelectionController.prototype,

    /** @override */
    getIndexBefore: function(index) {
      var result =
          GridSelectionController.prototype.getIndexBefore.call(this, index);
      return result == -1 ? this.getLastIndex() : result;
    },

    /** @override */
    getIndexAfter: function(index) {
      var result =
          GridSelectionController.prototype.getIndexAfter.call(this, index);
      return result == -1 ? this.getFirstIndex() : result;
    },

    /** @override */
    handleKeyDown: function(e) {
      if (e.keyIdentifier == 'Enter')
        cr.dispatchSimpleEvent(this.grid_, 'activate');
      else
        GridSelectionController.prototype.handleKeyDown.call(this, e);
    },
  };

  /**
   * Creates a new user images grid element.
   * @param {Object=} opt_propertyBag Optional properties.
   * @constructor
   * @extends {cr.ui.Grid}
   */
  var WallpaperThumbnailsGrid = cr.ui.define('grid');

  WallpaperThumbnailsGrid.prototype = {
    __proto__: Grid.prototype,

    /**
     * The checkbox element.
     */
    checkmark_: undefined,

    /**
     * ID of spinner delay timer.
     * @private
     */
    spinnerTimeout_: 0,

    /**
     * The item in data model which should have a checkmark.
     * @type {{baseURL: string, dynamicURL: string, layout: string,
     *         author: string, authorWebsite: string,
     *         availableOffline: boolean}}
     *     wallpaperInfo The information of the wallpaper to be set active.
     */
    activeItem_: undefined,
    set activeItem(activeItem) {
      if (this.activeItem_ != activeItem) {
        this.activeItem_ = activeItem;
        this.updateActiveThumb_();
      }
    },

    get activeItem() {
      return this.activeItem_;
    },

    /**
     * A unique ID that assigned to each set dataModel operation. Note that this
     * id wont increase if the new dataModel is null or empty.
     */
    dataModelId_: 0,

    /**
     * The number of items that need to be generated after a new dataModel is
     * set.
     */
    pendingItems_: 0,

    /**
     * Maintains all grid items' thumbnail images for quickly switching between
     * different categories.
     */
    thumbnailList_: undefined,

    /** @override */
    set dataModel(dataModel) {
      if (this.dataModel_ == dataModel)
        return;

      if (dataModel && dataModel.length != 0) {
        this.dataModelId_++;
        // Clears old pending items. The new pending items will be counted when
        // item is constructed in function itemConstructor below.
        this.pendingItems_ = 0;

        this.style.visibility = 'hidden';
        // If spinner is hidden, schedule to show the spinner after
        // ShowSpinnerDelayMs delay. Otherwise, keep it spinning.
        if ($('spinner-container').hidden) {
          this.spinnerTimeout_ = window.setTimeout(function() {
            $('spinner-container').hidden = false;
          }, ShowSpinnerDelayMs);
        }
      } else {
        // Sets dataModel to null should hide spinner immedidately.
        $('spinner-container').hidden = true;
      }

      var parentSetter = cr.ui.Grid.prototype.__lookupSetter__('dataModel');
      parentSetter.call(this, dataModel);
    },

    get dataModel() {
      return this.dataModel_;
    },

    /** @override */
    createSelectionController: function(sm) {
      return new WallpaperThumbnailsGridSelectionController(sm, this);
    },

    /**
     * Check if new thumbnail grid finished loading. This reduces the count of
     * remaining items to be loaded and when 0, shows the thumbnail grid. Note
     * it does not reduce the count on a previous |dataModelId|.
     * @param {number} dataModelId A unique ID that a thumbnail item is
     *     associated to.
     * @param {number} opt_wallpaperId The unique wallpaper ID that associated
     *     with this thumbnail gird item.
     * @param {object} opt_thumbnail The thumbnail image that associated with
     *     the opt_wallpaperId.
     */
    pendingItemComplete: function(dataModelId,
                                  opt_wallpaperId,
                                  opt_thumbnail) {
      if (dataModelId != this.dataModelId_)
        return;
      this.pendingItems_--;
      if (opt_wallpaperId != null)
        this.thumbnailList_[opt_wallpaperId] = opt_thumbnail;
      if (this.pendingItems_ == 0) {
        this.style.visibility = 'visible';
        window.clearTimeout(this.spinnerTimeout_);
        this.spinnerTimeout_ = 0;
        $('spinner-container').hidden = true;
      }
    },

    /** @override */
    decorate: function() {
      Grid.prototype.decorate.call(this);
      // checkmark_ needs to be initialized before set data model. Otherwise, we
      // may try to access checkmark before initialization in
      // updateActiveThumb_().
      this.checkmark_ = cr.doc.createElement('div');
      this.checkmark_.classList.add('check');
      this.dataModel = new ArrayDataModel([]);
      this.thumbnailList_ = new ArrayDataModel([]);
      var self = this;
      this.itemConstructor = function(value) {
        var dataModelId = self.dataModelId_;
        self.pendingItems_++;
        return WallpaperThumbnailsGridItem(value, dataModelId,
            self.thumbnailList_[value.wallpaperId],
            self.pendingItemComplete.bind(self));
      };
      this.selectionModel = new ListSingleSelectionModel();
      this.inProgramSelection_ = false;
    },

    /**
     * Should only be queried from the 'change' event listener, true if the
     * change event was triggered by a programmatical selection change.
     * @type {boolean}
     */
    get inProgramSelection() {
      return this.inProgramSelection_;
    },

    /**
     * Set index to the image selected.
     * @type {number} index The index of selected image.
     */
    set selectedItemIndex(index) {
      this.inProgramSelection_ = true;
      this.selectionModel.selectedIndex = index;
      this.inProgramSelection_ = false;
    },

    /**
     * The selected item.
     * @type {!Object} Wallpaper information inserted into the data model.
     */
    get selectedItem() {
      var index = this.selectionModel.selectedIndex;
      return index != -1 ? this.dataModel.item(index) : null;
    },
    set selectedItem(selectedItem) {
      var index = this.dataModel.indexOf(selectedItem);
      this.inProgramSelection_ = true;
      this.selectionModel.leadIndex = index;
      this.selectionModel.selectedIndex = index;
      this.inProgramSelection_ = false;
    },

    /**
     * Forces re-display, size re-calculation and focuses grid.
     */
    updateAndFocus: function() {
      // Recalculate the measured item size.
      this.measured_ = null;
      this.columns = 0;
      this.redraw();
      this.focus();
    },

    /**
     * Shows a checkmark on the active thumbnail and clears previous active one
     * if any. Note if wallpaper was not set successfully, checkmark should not
     * show on that thumbnail.
     */
    updateActiveThumb_: function() {
      var selectedGridItem = this.getListItem(this.activeItem_);
      if (this.checkmark_.parentNode &&
          this.checkmark_.parentNode == selectedGridItem) {
        return;
      }

      // Clears previous checkmark.
      if (this.checkmark_.parentNode)
        this.checkmark_.parentNode.removeChild(this.checkmark_);

      if (!selectedGridItem)
        return;
      selectedGridItem.appendChild(this.checkmark_);
    },

    /**
     * Redraws the viewport.
     */
    redraw: function() {
      Grid.prototype.redraw.call(this);
      // The active thumbnail maybe deleted in the above redraw(). Sets it again
      // to make sure checkmark shows correctly.
      this.updateActiveThumb_();
    }
  };

  return {
    WallpaperThumbnailsGrid: WallpaperThumbnailsGrid
  };
});