1// Copyright (c) 2012 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'use strict'; 6 7/** 8 * FileGrid constructor. 9 * 10 * Represents grid for the Grid View in the File Manager. 11 * @constructor 12 * @extends {cr.ui.Grid} 13 */ 14 15function FileGrid() { 16 throw new Error('Use FileGrid.decorate'); 17} 18 19/** 20 * Thumbnail quality. 21 * @enum {number} 22 */ 23FileGrid.ThumbnailQuality = { 24 LOW: 0, 25 HIGH: 1 26}; 27 28/** 29 * Inherits from cr.ui.Grid. 30 */ 31FileGrid.prototype.__proto__ = cr.ui.Grid.prototype; 32 33/** 34 * Decorates an HTML element to be a FileGrid. 35 * @param {HTMLElement} self The grid to decorate. 36 * @param {MetadataCache} metadataCache Metadata cache to find entries 37 * metadata. 38 * @param {VolumeManagerWrapper} volumeManager Volume manager instance. 39 */ 40FileGrid.decorate = function(self, metadataCache, volumeManager) { 41 cr.ui.Grid.decorate(self); 42 self.__proto__ = FileGrid.prototype; 43 self.metadataCache_ = metadataCache; 44 self.volumeManager_ = volumeManager; 45 46 self.scrollBar_ = new MainPanelScrollBar(); 47 self.scrollBar_.initialize(self.parentNode, self); 48 self.setBottomMarginForPanel(0); 49 50 self.itemConstructor = function(entry) { 51 var item = self.ownerDocument.createElement('LI'); 52 FileGrid.Item.decorate(item, entry, self); 53 return item; 54 }; 55 56 self.relayoutRateLimiter_ = 57 new AsyncUtil.RateLimiter(self.relayoutImmediately_.bind(self)); 58}; 59 60/** 61 * Updates items to reflect metadata changes. 62 * @param {string} type Type of metadata changed. 63 * @param {Array.<Entry>} entries Entries whose metadata changed. 64 */ 65FileGrid.prototype.updateListItemsMetadata = function(type, entries) { 66 var urls = util.entriesToURLs(entries); 67 var boxes = this.querySelectorAll('.img-container'); 68 for (var i = 0; i < boxes.length; i++) { 69 var box = boxes[i]; 70 var listItem = this.getListItemAncestor(box); 71 var entry = listItem && this.dataModel.item(listItem.listIndex); 72 if (!entry || urls.indexOf(entry.toURL()) === -1) 73 continue; 74 75 FileGrid.decorateThumbnailBox(box, 76 entry, 77 this.metadataCache_, 78 this.volumeManager_, 79 ThumbnailLoader.FillMode.FIT, 80 FileGrid.ThumbnailQuality.LOW); 81 } 82}; 83 84/** 85 * Redraws the UI. Skips multiple consecutive calls. 86 */ 87FileGrid.prototype.relayout = function() { 88 this.relayoutRateLimiter_.run(); 89}; 90 91/** 92 * Redraws the UI immediately. 93 * @private 94 */ 95FileGrid.prototype.relayoutImmediately_ = function() { 96 this.startBatchUpdates(); 97 this.columns = 0; 98 this.redraw(); 99 this.endBatchUpdates(); 100 cr.dispatchSimpleEvent(this, 'relayout'); 101}; 102 103/** 104 * Decorates thumbnail. 105 * @param {HTMLElement} li List item. 106 * @param {Entry} entry Entry to render a thumbnail for. 107 * @param {MetadataCache} metadataCache To retrieve metadata. 108 * @param {VolumeManagerWrapper} volumeManager Volume manager instance. 109 */ 110FileGrid.decorateThumbnail = function(li, entry, metadataCache, volumeManager) { 111 li.className = 'thumbnail-item'; 112 if (entry) 113 filelist.decorateListItem(li, entry, metadataCache); 114 115 var frame = li.ownerDocument.createElement('div'); 116 frame.className = 'thumbnail-frame'; 117 li.appendChild(frame); 118 119 var box = li.ownerDocument.createElement('div'); 120 if (entry) { 121 FileGrid.decorateThumbnailBox(box, 122 entry, 123 metadataCache, 124 volumeManager, 125 ThumbnailLoader.FillMode.AUTO, 126 FileGrid.ThumbnailQuality.LOW); 127 } 128 frame.appendChild(box); 129 130 var bottom = li.ownerDocument.createElement('div'); 131 bottom.className = 'thumbnail-bottom'; 132 bottom.appendChild(filelist.renderFileNameLabel(li.ownerDocument, entry)); 133 frame.appendChild(bottom); 134}; 135 136/** 137 * Decorates the box containing a centered thumbnail image. 138 * 139 * @param {HTMLDivElement} box Box to decorate. 140 * @param {Entry} entry Entry which thumbnail is generating for. 141 * @param {MetadataCache} metadataCache To retrieve metadata. 142 * @param {VolumeManagerWrapper} volumeManager Volume manager instance. 143 * @param {ThumbnailLoader.FillMode} fillMode Fill mode. 144 * @param {FileGrid.ThumbnailQuality} quality Thumbnail quality. 145 * @param {function(HTMLElement)=} opt_imageLoadCallback Callback called when 146 * the image has been loaded before inserting it into the DOM. 147 */ 148FileGrid.decorateThumbnailBox = function( 149 box, entry, metadataCache, volumeManager, fillMode, quality, 150 opt_imageLoadCallback) { 151 var locationInfo = volumeManager.getLocationInfo(entry); 152 box.className = 'img-container'; 153 154 if (entry.isDirectory) { 155 box.setAttribute('generic-thumbnail', 'folder'); 156 if (locationInfo && locationInfo.isDriveBased) { 157 metadataCache.getOne(entry, 'external', function(metadata) { 158 if (metadata.shared) 159 box.classList.add('shared'); 160 }); 161 } 162 if (opt_imageLoadCallback) 163 setTimeout(opt_imageLoadCallback, 0, null /* callback parameter */); 164 return; 165 } 166 167 var metadataTypes = 'thumbnail|filesystem|external|media'; 168 169 // Drive provides high quality thumbnails via USE_EMBEDDED, however local 170 // images usually provide very tiny thumbnails, therefore USE_EMBEDDE can't 171 // be used to obtain high quality output. 172 var useEmbedded; 173 switch (quality) { 174 case FileGrid.ThumbnailQuality.LOW: 175 useEmbedded = ThumbnailLoader.UseEmbedded.USE_EMBEDDED; 176 break; 177 case FileGrid.ThumbnailQuality.HIGH: 178 // TODO(mtomasz): Use Entry instead of paths. 179 useEmbedded = (locationInfo && locationInfo.isDriveBased) ? 180 ThumbnailLoader.UseEmbedded.USE_EMBEDDED : 181 ThumbnailLoader.UseEmbedded.NO_EMBEDDED; 182 break; 183 } 184 185 metadataCache.getOne(entry, metadataTypes, 186 function(metadata) { 187 new ThumbnailLoader(entry, 188 ThumbnailLoader.LoaderType.IMAGE, 189 metadata, 190 undefined, // opt_mediaType 191 useEmbedded). 192 load(box, 193 fillMode, 194 ThumbnailLoader.OptimizationMode.DISCARD_DETACHED, 195 opt_imageLoadCallback); 196 }); 197}; 198 199/** 200 * Item for the Grid View. 201 * @constructor 202 */ 203FileGrid.Item = function() { 204 throw new Error(); 205}; 206 207/** 208 * Inherits from cr.ui.ListItem. 209 */ 210FileGrid.Item.prototype.__proto__ = cr.ui.ListItem.prototype; 211 212Object.defineProperty(FileGrid.Item.prototype, 'label', { 213 /** 214 * @this {FileGrid.Item} 215 * @return {string} Label of the item. 216 */ 217 get: function() { 218 return this.querySelector('filename-label').textContent; 219 } 220}); 221 222/** 223 * @param {Element} li List item element. 224 * @param {Entry} entry File entry. 225 * @param {FileGrid} grid Owner. 226 */ 227FileGrid.Item.decorate = function(li, entry, grid) { 228 li.__proto__ = FileGrid.Item.prototype; 229 // TODO(mtomasz): Pass the metadata cache and the volume manager directly 230 // instead of accessing private members of grid. 231 FileGrid.decorateThumbnail( 232 li, entry, grid.metadataCache_, grid.volumeManager_, true); 233 234 // Override the default role 'listitem' to 'option' to match the parent's 235 // role (listbox). 236 li.setAttribute('role', 'option'); 237}; 238 239/** 240 * Sets the margin height for the transparent preview panel at the bottom. 241 * @param {number} margin Margin to be set in px. 242 */ 243FileGrid.prototype.setBottomMarginForPanel = function(margin) { 244 // +20 bottom margin is needed to match the bottom margin size with the 245 // margin between its items. 246 this.style.paddingBottom = (margin + 20) + 'px'; 247 this.scrollBar_.setBottomMarginForPanel(margin); 248}; 249 250/** 251 * Obtains if the drag selection should be start or not by referring the mouse 252 * event. 253 * @param {MouseEvent} event Drag start event. 254 * @return {boolean} True if the mouse is hit to the background of the list. 255 */ 256FileGrid.prototype.shouldStartDragSelection = function(event) { 257 var pos = DragSelector.getScrolledPosition(this, event); 258 return this.getHitElements(pos.x, pos.y).length === 0; 259}; 260 261/** 262 * Obtains the column/row index that the coordinate points. 263 * @param {number} coordinate Vertical/horizontal coordinate value that points 264 * column/row. 265 * @param {number} step Length from a column/row to the next one. 266 * @param {number} threshold Threshold that determines whether 1 offset is added 267 * to the return value or not. This is used in order to handle the margin of 268 * column/row. 269 * @return {number} Index of hit column/row. 270 * @private 271 */ 272FileGrid.prototype.getHitIndex_ = function(coordinate, step, threshold) { 273 var index = ~~(coordinate / step); 274 return (coordinate % step >= threshold) ? index + 1 : index; 275}; 276 277/** 278 * Obtains the index list of elements that are hit by the point or the 279 * rectangle. 280 * 281 * We should match its argument interface with FileList.getHitElements. 282 * 283 * @param {number} x X coordinate value. 284 * @param {number} y Y coordinate value. 285 * @param {number=} opt_width Width of the coordinate. 286 * @param {number=} opt_height Height of the coordinate. 287 * @return {Array.<number>} Index list of hit elements. 288 */ 289FileGrid.prototype.getHitElements = function(x, y, opt_width, opt_height) { 290 var currentSelection = []; 291 var right = x + (opt_width || 0); 292 var bottom = y + (opt_height || 0); 293 var itemMetrics = this.measureItem(); 294 var horizontalStartIndex = this.getHitIndex_( 295 x, itemMetrics.width, itemMetrics.width - itemMetrics.marginRight); 296 var horizontalEndIndex = Math.min(this.columns, this.getHitIndex_( 297 right, itemMetrics.width, itemMetrics.marginLeft)); 298 var verticalStartIndex = this.getHitIndex_( 299 y, itemMetrics.height, itemMetrics.height - itemMetrics.bottom); 300 var verticalEndIndex = this.getHitIndex_( 301 bottom, itemMetrics.height, itemMetrics.marginTop); 302 for (var verticalIndex = verticalStartIndex; 303 verticalIndex < verticalEndIndex; 304 verticalIndex++) { 305 var indexBase = this.getFirstItemInRow(verticalIndex); 306 for (var horizontalIndex = horizontalStartIndex; 307 horizontalIndex < horizontalEndIndex; 308 horizontalIndex++) { 309 var index = indexBase + horizontalIndex; 310 if (0 <= index && index < this.dataModel.length) 311 currentSelection.push(index); 312 } 313 } 314 return currentSelection; 315}; 316