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;
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
22 * @param {number} dataModelId A unique ID that this item associated to.
23 * @param {object} thumbnail The thumbnail image Object associated with this
25 * @param {function} callback The callback function when decoration finished.
27 * @extends {cr.ui.GridItem}
29 function WallpaperThumbnailsGridItem(wallpaperInfo,
33 var el = new GridItem(wallpaperInfo);
34 el.__proto__ = WallpaperThumbnailsGridItem.prototype;
35 el.dataModelId_ = dataModelId;
36 el.thumbnail_ = thumbnail;
37 el.callback_ = callback;
41 WallpaperThumbnailsGridItem.prototype = {
42 __proto__: GridItem.prototype,
45 * The unique ID this thumbnail grid associated to.
51 * The thumbnail image associated with the current grid item.
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.
64 decorate: function() {
65 GridItem.prototype.decorate.call(this);
66 // Removes garbage created by GridItem.
69 if (this.thumbnail_) {
70 this.appendChild(this.thumbnail_);
71 this.callback_(this.dataModelId_);
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);
82 switch (this.dataItem.source) {
83 case Constants.WallpaperSourceEnum.AddNew:
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;
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);
94 case Constants.WallpaperSourceEnum.Custom:
95 var errorHandler = function(e) {
96 self.callback_(self.dataModelId_);
97 console.error('Can not access file system.');
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,
107 var fallback = function() {
108 wallpaperDirectories.getDirectory(
109 Constants.WallpaperDirNameEnum.ORIGINAL, function(dirEntry) {
110 dirEntry.getFile(fileName, {create: false}, setURL,
114 var success = function(dirEntry) {
115 dirEntry.getFile(fileName, {create: false}, setURL, fallback);
117 wallpaperDirectories.getDirectory(
118 Constants.WallpaperDirNameEnum.THUMBNAIL, success, errorHandler);
120 getThumbnail(self.dataItem.baseURL);
122 case Constants.WallpaperSourceEnum.OEM:
123 case Constants.WallpaperSourceEnum.Online:
124 chrome.wallpaperPrivate.getThumbnail(this.dataItem.baseURL,
125 this.dataItem.source,
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,
135 window.URL.revokeObjectURL(this.src);
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';
143 xhr.addEventListener('load', function(e) {
144 if (xhr.status === 200) {
145 chrome.wallpaperPrivate.saveThumbnail(self.dataItem.baseURL,
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
153 imageEl.addEventListener('load', function(e) {
154 self.callback_(self.dataModelId_,
155 self.dataItem.wallpaperId,
157 window.URL.revokeObjectURL(this.src);
160 self.callback_(self.dataModelId_);
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);
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
180 * @param {cr.ui.Grid} grid The grid to interact with.
182 * @extends {cr.ui.GridSelectionController}
184 function WallpaperThumbnailsGridSelectionController(selectionModel, grid) {
185 GridSelectionController.call(this, selectionModel, grid);
188 WallpaperThumbnailsGridSelectionController.prototype = {
189 __proto__: GridSelectionController.prototype,
192 getIndexBefore: function(index) {
194 GridSelectionController.prototype.getIndexBefore.call(this, index);
195 return result == -1 ? this.getLastIndex() : result;
199 getIndexAfter: function(index) {
201 GridSelectionController.prototype.getIndexAfter.call(this, index);
202 return result == -1 ? this.getFirstIndex() : result;
206 handleKeyDown: function(e) {
207 if (e.keyIdentifier == 'Enter')
208 cr.dispatchSimpleEvent(this.grid_, 'activate');
210 GridSelectionController.prototype.handleKeyDown.call(this, e);
215 * Creates a new user images grid element.
216 * @param {Object=} opt_propertyBag Optional properties.
218 * @extends {cr.ui.Grid}
220 var WallpaperThumbnailsGrid = cr.ui.define('grid');
222 WallpaperThumbnailsGrid.prototype = {
223 __proto__: Grid.prototype,
226 * The checkbox element.
228 checkmark_: undefined,
231 * ID of spinner delay timer.
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.
243 activeItem_: undefined,
244 set activeItem(activeItem) {
245 if (this.activeItem_ != activeItem) {
246 this.activeItem_ = activeItem;
247 this.updateActiveThumb_();
252 return this.activeItem_;
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.
262 * The number of items that need to be generated after a new dataModel is
268 * Maintains all grid items' thumbnail images for quickly switching between
269 * different categories.
271 thumbnailList_: undefined,
274 set dataModel(dataModel) {
275 if (this.dataModel_ == dataModel)
278 if (dataModel && dataModel.length != 0) {
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);
293 // Sets dataModel to null should hide spinner immediately.
294 $('spinner-container').hidden = true;
297 var parentSetter = cr.ui.Grid.prototype.__lookupSetter__('dataModel');
298 parentSetter.call(this, dataModel);
302 return this.dataModel_;
306 createSelectionController: function(sm) {
307 return new WallpaperThumbnailsGridSelectionController(sm, this);
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
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.
321 pendingItemComplete: function(dataModelId,
324 if (dataModelId != this.dataModelId_)
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;
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([]);
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));
356 this.selectionModel = new ListSingleSelectionModel();
357 this.inProgramSelection_ = false;
361 * Should only be queried from the 'change' event listener, true if the
362 * change event was triggered by a programmatical selection change.
365 get inProgramSelection() {
366 return this.inProgramSelection_;
370 * Set index to the image selected.
371 * @type {number} index The index of selected image.
373 set selectedItemIndex(index) {
374 this.inProgramSelection_ = true;
375 this.selectionModel.selectedIndex = index;
376 this.inProgramSelection_ = false;
381 * @type {!Object} Wallpaper information inserted into the data model.
384 var index = this.selectionModel.selectedIndex;
385 return index != -1 ? this.dataModel.item(index) : null;
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;
396 * Forces re-display, size re-calculation and focuses grid.
398 updateAndFocus: function() {
399 // Recalculate the measured item size.
400 this.measured_ = null;
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.
411 updateActiveThumb_: function() {
412 var selectedGridItem = this.getListItem(this.activeItem_);
413 if (this.checkmark_.parentNode &&
414 this.checkmark_.parentNode == selectedGridItem) {
418 // Clears previous checkmark.
419 if (this.checkmark_.parentNode)
420 this.checkmark_.parentNode.removeChild(this.checkmark_);
422 if (!selectedGridItem)
424 selectedGridItem.appendChild(this.checkmark_);
428 * Redraws the viewport.
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_();
439 WallpaperThumbnailsGrid: WallpaperThumbnailsGrid