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